From b96d733a492ab0b94810b140b1a92e1b304b3512 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 17 Dec 2021 15:24:19 -0300 Subject: [PATCH 01/47] better exception --- skcriteria/madm/similarity.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index 6437129..a380b77 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -27,7 +27,7 @@ # CONSTANTS # ============================================================================= -_VALID_DISTANCES = [ +_VALID_DISTANCES_METRICS = [ "braycurtis", "canberra", "chebyshev", @@ -155,13 +155,16 @@ def metric(self): @metric.setter def metric(self, metric): - if not callable(metric) and metric not in _VALID_DISTANCES: - raise ValueError(f"Invalid metric '{metric}'") + if not callable(metric) and metric not in _VALID_DISTANCES_METRICS: + metrics = ", ".join(f"'{m}'" for m in _VALID_DISTANCES_METRICS) + raise ValueError( + f"Invalid metric '{metric}'. Plese choose from: {metrics}" + ) self._metric = metric @property def cdist_kwargs(self): - """Extra parameters for the ``scipy.spatial.distance.cdist()``.""" + """Extra parameters for ``scipy.spatial.distance.cdist()`` function.""" return self._cdist_kwargs @cdist_kwargs.setter From 2d79af631e4f892e4b96c4f1bea4323c4d8e92a1 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 17 Dec 2021 19:22:43 -0300 Subject: [PATCH 02/47] repr improved --- skcriteria/core/data.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 8fd76b8..14adc8e 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -621,10 +621,26 @@ def __repr__(self): header = self._get_cow_headers() dimensions = self._get_axc_dimensions() - kwargs = {"header": header, "show_dimensions": False} + max_rows = pd.get_option("display.max_rows") + min_rows = pd.get_option("display.min_rows") + max_cols = pd.get_option("display.max_columns") + max_colwidth = pd.get_option("display.max_colwidth") + + width = ( + pd.io.formats.console.get_console_size()[0] + if pd.get_option("display.expand_frame_repr") + else None + ) - # retrieve the original string - original_string = self._data_df.to_string(**kwargs) + original_string = self._data_df.to_string( + max_rows=max_rows, + min_rows=min_rows, + max_cols=max_cols, + line_width=width, + max_colwidth=max_colwidth, + show_dimensions=False, + header=header, + ) # add dimension string = f"{original_string}\n[{dimensions}]" @@ -655,8 +671,7 @@ def _repr_html_(self): d = pq.PyQuery(html) for th in d("div.decisionmatrix table.dataframe > thead > tr > th"): crit = th.text - if crit: - th.text = header[crit] + th.text = header.get(crit, crit) return str(d) From 574603dc20d8b472b5ab175831efac9ed0936fe2 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 21 Jan 2022 19:23:06 -0300 Subject: [PATCH 03/47] copyright --- .header-template | 1 + setup.py | 1 + skcriteria/__init__.py | 1 + skcriteria/core/__init__.py | 1 + skcriteria/core/data.py | 1 + skcriteria/core/methods.py | 1 + skcriteria/core/plot.py | 1 + skcriteria/madm/__init__.py | 1 + skcriteria/madm/electre.py | 1 + skcriteria/madm/moora.py | 1 + skcriteria/madm/similarity.py | 1 + skcriteria/madm/simple.py | 1 + skcriteria/madm/simus.py | 1 + skcriteria/pipeline.py | 1 + skcriteria/preprocessing/__init__.py | 1 + skcriteria/preprocessing/distance.py | 1 + skcriteria/preprocessing/increment.py | 1 + skcriteria/preprocessing/invert_objectives.py | 1 + skcriteria/preprocessing/push_negatives.py | 1 + skcriteria/preprocessing/scalers.py | 1 + skcriteria/preprocessing/weighters.py | 1 + skcriteria/utils/__init__.py | 1 + skcriteria/utils/bunch.py | 1 + skcriteria/utils/decorators.py | 1 + skcriteria/utils/lp.py | 3 ++- skcriteria/utils/rank.py | 1 + tests/conftest.py | 1 + tests/core/test_data.py | 1 + tests/core/test_methods.py | 1 + tests/core/test_plot.py | 1 + tests/madm/test_electre.py | 1 + tests/madm/test_moora.py | 1 + tests/madm/test_similarity.py | 1 + tests/madm/test_simple.py | 1 + tests/madm/test_simus.py | 1 + tests/preprocessing/test_distance.py | 1 + tests/preprocessing/test_increment.py | 1 + tests/preprocessing/test_invert_objectives.py | 1 + tests/preprocessing/test_push_negatives.py | 1 + tests/preprocessing/test_scalers.py | 1 + tests/preprocessing/test_weighters.py | 1 + tests/test_pipeline.py | 1 + tests/utils/test_bunch.py | 1 + tests/utils/test_decorators.py | 1 + tests/utils/test_lp.py | 1 + tests/utils/test_rank.py | 1 + tools/checkapidocsdir.py | 1 + tools/checkheader.py | 1 + tools/checktestdir.py | 1 + 49 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.header-template b/.header-template index af4ae38..f12b724 100644 --- a/.header-template +++ b/.header-template @@ -2,4 +2,5 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. \ No newline at end of file diff --git a/setup.py b/setup.py index 0913b10..5d12374 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/__init__.py b/skcriteria/__init__.py index 1a24fdf..16e7540 100644 --- a/skcriteria/__init__.py +++ b/skcriteria/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/core/__init__.py b/skcriteria/core/__init__.py index 8e47daf..79ac179 100644 --- a/skcriteria/core/__init__.py +++ b/skcriteria/core/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 14adc8e..3f71caa 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index fd1b725..21e58b3 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/core/plot.py b/skcriteria/core/plot.py index b5d8ebc..26ba167 100644 --- a/skcriteria/core/plot.py +++ b/skcriteria/core/plot.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/__init__.py b/skcriteria/madm/__init__.py index 075c632..340d44f 100644 --- a/skcriteria/madm/__init__.py +++ b/skcriteria/madm/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/electre.py b/skcriteria/madm/electre.py index 470dac0..45981bd 100644 --- a/skcriteria/madm/electre.py +++ b/skcriteria/madm/electre.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/moora.py b/skcriteria/madm/moora.py index 822ea59..930d5dc 100644 --- a/skcriteria/madm/moora.py +++ b/skcriteria/madm/moora.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index a380b77..4f4e738 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/simple.py b/skcriteria/madm/simple.py index 5e9bfd3..8d57e2d 100644 --- a/skcriteria/madm/simple.py +++ b/skcriteria/madm/simple.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/madm/simus.py b/skcriteria/madm/simus.py index dc53cf4..d06a5c6 100644 --- a/skcriteria/madm/simus.py +++ b/skcriteria/madm/simus.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index 8e9268e..c1b57cd 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/__init__.py b/skcriteria/preprocessing/__init__.py index b7218ed..24c1611 100644 --- a/skcriteria/preprocessing/__init__.py +++ b/skcriteria/preprocessing/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/distance.py b/skcriteria/preprocessing/distance.py index 9a53255..d7c0319 100644 --- a/skcriteria/preprocessing/distance.py +++ b/skcriteria/preprocessing/distance.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/increment.py b/skcriteria/preprocessing/increment.py index 27c161a..89c049d 100644 --- a/skcriteria/preprocessing/increment.py +++ b/skcriteria/preprocessing/increment.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/invert_objectives.py b/skcriteria/preprocessing/invert_objectives.py index 1462254..0885dbb 100644 --- a/skcriteria/preprocessing/invert_objectives.py +++ b/skcriteria/preprocessing/invert_objectives.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/push_negatives.py b/skcriteria/preprocessing/push_negatives.py index 4aca7d7..763a8fd 100644 --- a/skcriteria/preprocessing/push_negatives.py +++ b/skcriteria/preprocessing/push_negatives.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/scalers.py b/skcriteria/preprocessing/scalers.py index 8dc6f7e..03e6c4f 100644 --- a/skcriteria/preprocessing/scalers.py +++ b/skcriteria/preprocessing/scalers.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index 5ca39e5..c88648a 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/utils/__init__.py b/skcriteria/utils/__init__.py index 7f21e3e..d479c10 100644 --- a/skcriteria/utils/__init__.py +++ b/skcriteria/utils/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/utils/bunch.py b/skcriteria/utils/bunch.py index b7d7d05..8396ba1 100644 --- a/skcriteria/utils/bunch.py +++ b/skcriteria/utils/bunch.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/utils/decorators.py b/skcriteria/utils/decorators.py index fb3b8d6..d64f967 100644 --- a/skcriteria/utils/decorators.py +++ b/skcriteria/utils/decorators.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/skcriteria/utils/lp.py b/skcriteria/utils/lp.py index 3b306d1..85fc3da 100644 --- a/skcriteria/utils/lp.py +++ b/skcriteria/utils/lp.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= @@ -154,7 +155,7 @@ class _LPBase: .. code-block:: python - model = lp.Maximize( or lp.Minimize + model = lp.Maximize( # or lp.Minimize z=250 * x0 + 130 * x1 + 350 * x2, solver=solver ).subject_to( 120 * x0 + 200 * x1 + 340 * x2 <= 500, diff --git a/skcriteria/utils/rank.py b/skcriteria/utils/rank.py index 303039d..eed27da 100644 --- a/skcriteria/utils/rank.py +++ b/skcriteria/utils/rank.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/conftest.py b/tests/conftest.py index 8ca5f89..2dfca6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/core/test_data.py b/tests/core/test_data.py index dc09cf0..ff3e182 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 5a9cc1b..b0f78a3 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/core/test_plot.py b/tests/core/test_plot.py index 54ddb79..fe6651e 100644 --- a/tests/core/test_plot.py +++ b/tests/core/test_plot.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/madm/test_electre.py b/tests/madm/test_electre.py index a2d217c..8131c51 100644 --- a/tests/madm/test_electre.py +++ b/tests/madm/test_electre.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/madm/test_moora.py b/tests/madm/test_moora.py index 7602032..a764501 100644 --- a/tests/madm/test_moora.py +++ b/tests/madm/test_moora.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/madm/test_similarity.py b/tests/madm/test_similarity.py index 701eca9..00aae7c 100644 --- a/tests/madm/test_similarity.py +++ b/tests/madm/test_similarity.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/madm/test_simple.py b/tests/madm/test_simple.py index e2c84c3..c516dce 100644 --- a/tests/madm/test_simple.py +++ b/tests/madm/test_simple.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/madm/test_simus.py b/tests/madm/test_simus.py index 74b2d69..3ecea97 100644 --- a/tests/madm/test_simus.py +++ b/tests/madm/test_simus.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_distance.py b/tests/preprocessing/test_distance.py index 3465e86..d0ecc23 100644 --- a/tests/preprocessing/test_distance.py +++ b/tests/preprocessing/test_distance.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_increment.py b/tests/preprocessing/test_increment.py index 68da01b..b97c5ea 100644 --- a/tests/preprocessing/test_increment.py +++ b/tests/preprocessing/test_increment.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_invert_objectives.py b/tests/preprocessing/test_invert_objectives.py index 5c19105..b3f8f72 100644 --- a/tests/preprocessing/test_invert_objectives.py +++ b/tests/preprocessing/test_invert_objectives.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_push_negatives.py b/tests/preprocessing/test_push_negatives.py index 344ccc7..7543ae1 100644 --- a/tests/preprocessing/test_push_negatives.py +++ b/tests/preprocessing/test_push_negatives.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_scalers.py b/tests/preprocessing/test_scalers.py index f35413d..1ab8c01 100644 --- a/tests/preprocessing/test_scalers.py +++ b/tests/preprocessing/test_scalers.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/preprocessing/test_weighters.py b/tests/preprocessing/test_weighters.py index 99cacc1..b5f9498 100644 --- a/tests/preprocessing/test_weighters.py +++ b/tests/preprocessing/test_weighters.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index e81ca9e..618c410 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/utils/test_bunch.py b/tests/utils/test_bunch.py index 4584553..f77df9a 100644 --- a/tests/utils/test_bunch.py +++ b/tests/utils/test_bunch.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/utils/test_decorators.py b/tests/utils/test_decorators.py index c2b27c0..f51ea11 100644 --- a/tests/utils/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/utils/test_lp.py b/tests/utils/test_lp.py index f1103e7..7ea62a6 100644 --- a/tests/utils/test_lp.py +++ b/tests/utils/test_lp.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tests/utils/test_rank.py b/tests/utils/test_rank.py index 487b9e2..ae0a473 100644 --- a/tests/utils/test_rank.py +++ b/tests/utils/test_rank.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tools/checkapidocsdir.py b/tools/checkapidocsdir.py index 1a2faf3..d7f23bd 100644 --- a/tools/checkapidocsdir.py +++ b/tools/checkapidocsdir.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tools/checkheader.py b/tools/checkheader.py index d30dcaa..b505454 100644 --- a/tools/checkheader.py +++ b/tools/checkheader.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= diff --git a/tools/checktestdir.py b/tools/checktestdir.py index 70a34eb..2dc5e29 100644 --- a/tools/checktestdir.py +++ b/tools/checktestdir.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) # Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe # All rights reserved. # ============================================================================= From 68d7090cd82dff76042b125d815fa8dea78880b5 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 21 Jan 2022 19:35:20 -0300 Subject: [PATCH 04/47] copyright --- LICENSE.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE.txt b/LICENSE.txt index 71a0a9a..493f1cb 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,5 @@ Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +Copyright (c) 2022, QuatroPe All rights reserved. Redistribution and use in source and binary forms, with or without From 12c592acdd6ba2fe1479f63f299931282977f616 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 21 Jan 2022 19:35:46 -0300 Subject: [PATCH 05/47] quatropebadge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6557b39..6f2505c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - +[![QuatroPe](https://img.shields.io/badge/QuatroPe-Applications-1c5896)](https://quatrope.github.io/) [![Gihub Actions CI](https://github.com/quatrope/scikit-criteria/actions/workflows/CI.yml/badge.svg)](https://github.com/quatrope/scikit-criteria/actions/workflows/CI.yml) [![Documentation Status](https://readthedocs.org/projects/scikit-criteria/badge/?version=latest&style=flat)](http://scikit-criteria.readthedocs.io) [![PyPI](https://img.shields.io/pypi/v/scikit-criteria)](https://pypi.org/project/scikit-criteria/) From fe16776e155f707abd63073f8ae4d68e0dba66a2 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 24 Jan 2022 15:11:05 -0300 Subject: [PATCH 06/47] models --- ahp_ext/tuto.ipynb | 36 +++----- skcriteria/utils/hmodel_abc.py | 146 +++++++++++++++++++++++++++++++++ tests/utils/test_hmodel_abc.py | 71 ++++++++++++++++ 3 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 skcriteria/utils/hmodel_abc.py create mode 100644 tests/utils/test_hmodel_abc.py diff --git a/ahp_ext/tuto.ipynb b/ahp_ext/tuto.ipynb index e9ee526..6acb52a 100644 --- a/ahp_ext/tuto.ipynb +++ b/ahp_ext/tuto.ipynb @@ -138,10 +138,10 @@ } ], "source": [ - "# et’s asume we know in our case, that the importance of \n", - "# the autonomy is the 50%, the confort only a 5% and \n", + "# et’s asume we know in our case, that the importance of\n", + "# the autonomy is the 50%, the confort only a 5% and\n", "# the price is 45%\n", - "weights=[.5, .05, .45]\n", + "weights = [0.5, 0.05, 0.45]\n", "weights" ] }, @@ -237,10 +237,7 @@ } ], "source": [ - "mtx = ahp.t(\n", - " [[1],\n", - " [1., 1],\n", - " [1/3.0, 1/6.0, 1]])\n", + "mtx = ahp.t([[1], [1.0, 1], [1 / 3.0, 1 / 6.0, 1]])\n", "mtx" ] }, @@ -271,7 +268,7 @@ "outputs": [], "source": [ "# this validate the data\n", - "ahp.validate_ahp_matrix(3, mtx) " + "ahp.validate_ahp_matrix(3, mtx)" ] }, { @@ -325,7 +322,7 @@ } ], "source": [ - "ahp.validate_ahp_matrix(3, invalid_mtx) " + "ahp.validate_ahp_matrix(3, invalid_mtx)" ] }, { @@ -378,7 +375,7 @@ } ], "source": [ - "ahp.validate_ahp_matrix(3, invalid_mtx) " + "ahp.validate_ahp_matrix(3, invalid_mtx)" ] }, { @@ -409,11 +406,7 @@ } ], "source": [ - "crit_vs_crit = ahp.t([\n", - " [1.], \n", - " [1./3., 1.], \n", - " [1./3., 1./2., 1.]\n", - "])\n", + "crit_vs_crit = ahp.t([[1.0], [1.0 / 3.0, 1.0], [1.0 / 3.0, 1.0 / 2.0, 1.0]])\n", "crit_vs_crit" ] }, @@ -450,16 +443,9 @@ ], "source": [ "alt_vs_alt_by_crit = [\n", - " ahp.t([[1.], \n", - " [1./5., 1.], \n", - " [1./3., 3., 1.]]),\n", - " ahp.t([\n", - " [1.], \n", - " [9., 1.], \n", - " [3., 1./5., 1.]]),\n", - " ahp.t([[1.], \n", - " [1/2., 1.], \n", - " [5., 7., 1.]]),\n", + " ahp.t([[1.0], [1.0 / 5.0, 1.0], [1.0 / 3.0, 3.0, 1.0]]),\n", + " ahp.t([[1.0], [9.0, 1.0], [3.0, 1.0 / 5.0, 1.0]]),\n", + " ahp.t([[1.0], [1 / 2.0, 1.0], [5.0, 7.0, 1.0]]),\n", "]\n", "\n", "alt_vs_alt_by_crit" diff --git a/skcriteria/utils/hmodel_abc.py b/skcriteria/utils/hmodel_abc.py new file mode 100644 index 0000000..7ae1368 --- /dev/null +++ b/skcriteria/utils/hmodel_abc.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Framework for defining models with hyperparameters.""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +from abc import ABCMeta + +import attr + + +# ============================================================================= +# CONSTANTS +# ============================================================================= + +HMODEL_METADATA_KEY = "__hmodel__" + +HMODEL_CONFIG = "__hmodel_config__" + +DEFAULT_HMODEL_CONFIG = {"repr": False, "frozen": False} + + +# ============================================================================= +# FUNCTIONS +# ============================================================================= + + +def hparam(default, **kwargs): + """Create a hyper parameter. + + By design decision, hyper-parameter is required to have a sensitive default + value. + + Parameters + ---------- + default : + Sensitive default value of the hyper-parameter. + **kwargs : + Additional keyword arguments are passed and are documented in + ``attr.ib()``. + + Return + ------ + Hyper parameter with a default value. + + Notes + ----- + This function is a thin-wrapper over the attrs function ``attr.ib()``. + """ + field_metadata = kwargs.pop("metadata", {}) + model_metadata = field_metadata.setdefault(HMODEL_METADATA_KEY, {}) + model_metadata["hparam"] = True + return attr.field( + default=default, metadata=field_metadata, kw_only=True, **kwargs + ) + + +def mproperty(**kwargs): + """Create a internal property for the model. + + By design decision, hyper-parameter is required to have a sensitive default + value. + + Parameters + ---------- + default : + Sensitive default value of the hyper-parameter. + **kwargs : + Additional keyword arguments are passed and are documented in + ``attr.ib()``. + + Return + ------ + Hyper parameter with a default value. + + Notes + ----- + This function is a thin-wrapper over the attrs function ``attr.ib()``. + + """ + field_metadata = kwargs.pop("metadata", {}) + model_metadata = field_metadata.setdefault(HMODEL_METADATA_KEY, {}) + model_metadata["mproperty"] = True + return attr.field(init=False, metadata=field_metadata, **kwargs) + + +@attr.define(repr=False) +class HModelABC(metaclass=ABCMeta): + """Create a new models with hyperparameters.""" + + def __init_subclass__(cls): + """Initiate of subclasses. + + It ensures that every inherited class is decorated by ``attr.define()`` + and assigns as class configuration the parameters defined in the class + variable `__hmodel_config__`. + + In other words it is slightly equivalent to: + + .. code-block:: python + + @attr.s(**HModelABC.__hmodel_config__) + class Decomposer(HModelABC): + pass + + """ + model_config = getattr(cls, HMODEL_CONFIG, DEFAULT_HMODEL_CONFIG) + return attr.define(maybe_cls=cls, slots=False, **model_config) + + @classmethod + def get_hparams(cls): + """Return a tuple of available hyper parameters.""" + + def flt(f): + return f.metadata[HMODEL_METADATA_KEY].get("hparam", False) + + return tuple(f.name for f in attr.fields(cls) if flt(f)) + + def __repr__(self): + """x.__repr__() <==> repr(x).""" + + clsname = type(self).__name__ + + hparams = self.get_hparams() + selfd = attr.asdict( + self, + recurse=False, + filter=lambda attr, _: attr.name in hparams and attr.repr, + ) + + attrs_str = ", ".join( + [f"{k}={repr(v)}" for k, v in sorted(selfd.items())] + ) + return f"{clsname}({attrs_str})" diff --git a/tests/utils/test_hmodel_abc.py b/tests/utils/test_hmodel_abc.py new file mode 100644 index 0000000..de683a1 --- /dev/null +++ b/tests/utils/test_hmodel_abc.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.utils.hmodel_abc. + +""" + +# ============================================================================= +# IMPORTS +# ============================================================================= + + +import pytest + +from skcriteria.utils import hmodel_abc + +# ============================================================================= +# tests +# ============================================================================= + + +def test_hparam(): + hp = hmodel_abc.hparam(1) + + assert hp.metadata[hmodel_abc.HMODEL_METADATA_KEY]["hparam"] + + +def test_mproperty(): + mp = hmodel_abc.mproperty() + assert mp.metadata[hmodel_abc.HMODEL_METADATA_KEY]["mproperty"] + + +def test_create_HModel(): + class MyBase(hmodel_abc.HModelABC): + v = hmodel_abc.hparam(42) + + class MyModel(MyBase): + + p = hmodel_abc.hparam(25) + m = hmodel_abc.mproperty() + + @m.default + def _md(self): + return self.p + 1 + + model = MyModel() + assert model.v == 42 + assert model.p == 25 + assert model.m == 26 + + assert repr(model) == "MyModel(p=25, v=42)" + + model = MyModel(p=27, v=43) + assert model.v == 43 + assert model.p == 27 + assert model.m == 28 + assert repr(model) == "MyModel(p=27, v=43)" + + with pytest.raises(TypeError): + MyModel(27) + + with pytest.raises(TypeError): + MyModel(m=27) From ecdfdbd5eee6e0879d492f3c893a547604870873 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 24 Jan 2022 15:24:13 -0300 Subject: [PATCH 07/47] hmodel implementhed --- skcriteria/utils/hmodel_abc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skcriteria/utils/hmodel_abc.py b/skcriteria/utils/hmodel_abc.py index 7ae1368..408d979 100644 --- a/skcriteria/utils/hmodel_abc.py +++ b/skcriteria/utils/hmodel_abc.py @@ -130,7 +130,6 @@ def flt(f): def __repr__(self): """x.__repr__() <==> repr(x).""" - clsname = type(self).__name__ hparams = self.get_hparams() From 20ca39ee9466ac82ddee2c6dbb5be5d9c019143b Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 24 Jan 2022 15:24:18 -0300 Subject: [PATCH 08/47] hmodel implementhed --- docs/source/api/utils/hmodel_abc.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/source/api/utils/hmodel_abc.rst diff --git a/docs/source/api/utils/hmodel_abc.rst b/docs/source/api/utils/hmodel_abc.rst new file mode 100644 index 0000000..591cb0f --- /dev/null +++ b/docs/source/api/utils/hmodel_abc.rst @@ -0,0 +1,7 @@ +``skcriteria.utils.hmodel_abc`` module +====================================== + +.. automodule:: skcriteria.utils.hmodel_abc + :members: + :undoc-members: + :show-inheritance: From 47a2521b62057214f57582b4d870defb646bf68f Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 24 Jan 2022 15:29:46 -0300 Subject: [PATCH 09/47] refactor --- docs/source/api/utils/hmodel_abc.rst | 7 ------ skcriteria/utils/{hmodel_abc.py => hmodel.py} | 0 .../{test_hmodel_abc.py => test_hmodel.py} | 22 ++++++++++--------- 3 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 docs/source/api/utils/hmodel_abc.rst rename skcriteria/utils/{hmodel_abc.py => hmodel.py} (100%) rename tests/utils/{test_hmodel_abc.py => test_hmodel.py} (76%) diff --git a/docs/source/api/utils/hmodel_abc.rst b/docs/source/api/utils/hmodel_abc.rst deleted file mode 100644 index 591cb0f..0000000 --- a/docs/source/api/utils/hmodel_abc.rst +++ /dev/null @@ -1,7 +0,0 @@ -``skcriteria.utils.hmodel_abc`` module -====================================== - -.. automodule:: skcriteria.utils.hmodel_abc - :members: - :undoc-members: - :show-inheritance: diff --git a/skcriteria/utils/hmodel_abc.py b/skcriteria/utils/hmodel.py similarity index 100% rename from skcriteria/utils/hmodel_abc.py rename to skcriteria/utils/hmodel.py diff --git a/tests/utils/test_hmodel_abc.py b/tests/utils/test_hmodel.py similarity index 76% rename from tests/utils/test_hmodel_abc.py rename to tests/utils/test_hmodel.py index de683a1..7ca1da5 100644 --- a/tests/utils/test_hmodel_abc.py +++ b/tests/utils/test_hmodel.py @@ -9,7 +9,7 @@ # DOCS # ============================================================================= -"""test for skcriteria.utils.hmodel_abc. +"""test for skcriteria.utils.hmodel. """ @@ -20,7 +20,7 @@ import pytest -from skcriteria.utils import hmodel_abc +from skcriteria.utils import hmodel # ============================================================================= # tests @@ -28,29 +28,31 @@ def test_hparam(): - hp = hmodel_abc.hparam(1) + hp = hmodel.hparam(1) - assert hp.metadata[hmodel_abc.HMODEL_METADATA_KEY]["hparam"] + assert hp.metadata[hmodel.HMODEL_METADATA_KEY]["hparam"] def test_mproperty(): - mp = hmodel_abc.mproperty() - assert mp.metadata[hmodel_abc.HMODEL_METADATA_KEY]["mproperty"] + mp = hmodel.mproperty() + assert mp.metadata[hmodel.HMODEL_METADATA_KEY]["mproperty"] def test_create_HModel(): - class MyBase(hmodel_abc.HModelABC): - v = hmodel_abc.hparam(42) + class MyBase(hmodel.HModelABC): + v = hmodel.hparam(42) class MyModel(MyBase): - p = hmodel_abc.hparam(25) - m = hmodel_abc.mproperty() + p = hmodel.hparam(25) + m = hmodel.mproperty() @m.default def _md(self): return self.p + 1 + assert MyModel.get_hparams() == ("v", "p") + model = MyModel() assert model.v == 42 assert model.p == 25 From 6cd8466a60f6bf750b9f3fc212f3ae0316ae691a Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 24 Jan 2022 15:29:57 -0300 Subject: [PATCH 10/47] refactor --- docs/source/api/utils/hmodel.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/source/api/utils/hmodel.rst diff --git a/docs/source/api/utils/hmodel.rst b/docs/source/api/utils/hmodel.rst new file mode 100644 index 0000000..84629a0 --- /dev/null +++ b/docs/source/api/utils/hmodel.rst @@ -0,0 +1,7 @@ +``skcriteria.utils.hmodel`` module +====================================== + +.. automodule:: skcriteria.utils.hmodel + :members: + :undoc-members: + :show-inheritance: From 06c499ae996d49b1861868a2293fce9b4699a667 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 25 Jan 2022 12:49:51 -0300 Subject: [PATCH 11/47] fix #31 --- docs/source/api/utils/hmodel.rst | 7 -- skcriteria/core/data.py | 2 +- skcriteria/core/methods.py | 14 +-- skcriteria/madm/electre.py | 12 +-- skcriteria/madm/similarity.py | 20 ++-- skcriteria/madm/simus.py | 24 ++--- skcriteria/preprocessing/increment.py | 6 +- skcriteria/preprocessing/weighters.py | 27 ++--- skcriteria/utils/hmodel.py | 145 -------------------------- tests/core/test_methods.py | 10 +- tests/utils/test_hmodel.py | 73 ------------- 11 files changed, 36 insertions(+), 304 deletions(-) delete mode 100644 docs/source/api/utils/hmodel.rst delete mode 100644 skcriteria/utils/hmodel.py delete mode 100644 tests/utils/test_hmodel.py diff --git a/docs/source/api/utils/hmodel.rst b/docs/source/api/utils/hmodel.rst deleted file mode 100644 index 84629a0..0000000 --- a/docs/source/api/utils/hmodel.rst +++ /dev/null @@ -1,7 +0,0 @@ -``skcriteria.utils.hmodel`` module -====================================== - -.. automodule:: skcriteria.utils.hmodel - :members: - :undoc-members: - :show-inheritance: diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 3f71caa..09cf1a8 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -407,7 +407,7 @@ def copy(self, **kwargs): ---------- kwargs : The same parameters supported by ``from_mcda_data()``. The values - provided replace the existing ones in the obSject to be copied. + provided replace the existing ones in the object to be copied. Returns ------- diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index 21e58b3..391f28b 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -151,15 +151,6 @@ class SKCMatrixAndWeightTransformerABC(SKCTransformerABC): _TARGET_BOTH = "both" def __init__(self, target): - self.target = target - - @property - def target(self): - """Determine which part of the DecisionMatrix will be transformed.""" - return self._target - - @target.setter - def target(self, target): if target not in ( self._TARGET_MATRIX, self._TARGET_WEIGHTS, @@ -172,6 +163,11 @@ def target(self, target): ) self._target = target + @property + def target(self): + """Determine which part of the DecisionMatrix will be transformed.""" + return self._target + @abc.abstractmethod def _transform_weights(self, weights): """Execute the transform method over the weights. diff --git a/skcriteria/madm/electre.py b/skcriteria/madm/electre.py index 45981bd..0ac15d0 100644 --- a/skcriteria/madm/electre.py +++ b/skcriteria/madm/electre.py @@ -145,27 +145,19 @@ class ELECTRE1(SKCDecisionMakerABC): """ def __init__(self, p=0.65, q=0.35): - self.p = p - self.q = q + self._p = float(p) + self._q = float(q) @property def p(self): """Concordance threshold.""" return self._p - @p.setter - def p(self, p): - self._p = float(p) - @property def q(self): """Discordance threshold.""" return self._q - @q.setter - def q(self, q): - self._q = float(q) - @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): kernel, outrank, matrix_concordance, matrix_discordance = electre1( diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index 4f4e738..67fa5eb 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -146,32 +146,24 @@ class TOPSIS(SKCDecisionMakerABC): """ def __init__(self, *, metric="euclidean", **cdist_kwargs): - self.metric = metric - self.cdist_kwargs = cdist_kwargs - - @property - def metric(self): - """Which distance metric will be used.""" - return self._metric - - @metric.setter - def metric(self, metric): if not callable(metric) and metric not in _VALID_DISTANCES_METRICS: metrics = ", ".join(f"'{m}'" for m in _VALID_DISTANCES_METRICS) raise ValueError( f"Invalid metric '{metric}'. Plese choose from: {metrics}" ) self._metric = metric + self._cdist_kwargs = cdist_kwargs + + @property + def metric(self): + """Which distance metric will be used.""" + return self._metric @property def cdist_kwargs(self): """Extra parameters for ``scipy.spatial.distance.cdist()`` function.""" return self._cdist_kwargs - @cdist_kwargs.setter - def cdist_kwargs(self, cdist_kwargs): - self._cdist_kwargs = dict(cdist_kwargs) - @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): if Objective.MIN.value in objectives: diff --git a/skcriteria/madm/simus.py b/skcriteria/madm/simus.py index d06a5c6..94aaad0 100644 --- a/skcriteria/madm/simus.py +++ b/skcriteria/madm/simus.py @@ -248,34 +248,26 @@ class SIMUS(SKCDecisionMakerABC): """ def __init__(self, *, rank_by=1, solver="pulp"): - self.solver = solver - self.rank_by = rank_by - - @property - def solver(self): - """Solver used by PuLP.""" - return self._solver - - @solver.setter - def solver(self, solver): if not ( isinstance(solver, lp.pulp.LpSolver) or lp.is_solver_available(solver) ): raise ValueError(f"solver {solver} not available") self._solver = solver + if rank_by not in (1, 2): + raise ValueError("'rank_by' must be 1 or 2") + self._rank_by = rank_by + + @property + def solver(self): + """Solver used by PuLP.""" + return self._solver @property def rank_by(self): """Which of the two ranking provided by SIMUS is used.""" return self._rank_by - @rank_by.setter - def rank_by(self, rank_by): - if rank_by not in (1, 2): - raise ValueError("'rank_by' must be 1 or 2") - self._rank_by = rank_by - @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, b, weights, **kwargs): if len(np.unique(weights)) > 1: diff --git a/skcriteria/preprocessing/increment.py b/skcriteria/preprocessing/increment.py index 89c049d..27ad934 100644 --- a/skcriteria/preprocessing/increment.py +++ b/skcriteria/preprocessing/increment.py @@ -89,17 +89,13 @@ class AddValueToZero(SKCMatrixAndWeightTransformerABC): def __init__(self, value, target): super().__init__(target=target) - self.value = value + self._eps = float(value) @property def value(self): """Value to add to the matrix/weight when a zero is found.""" return self._eps - @value.setter - def value(self, value): - self._eps = float(value) - @doc_inherit(SKCMatrixAndWeightTransformerABC._transform_weights) def _transform_weights(self, weights): return add_value_to_zero(weights, value=self.value, axis=None) diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index c88648a..f979783 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -87,17 +87,13 @@ class EqualWeighter(SKCWeighterABC): """ def __init__(self, base_value=1): - self.base_value = base_value + self._base_value = float(base_value) @property def base_value(self): """Value to be normalized by the number of criteria.""" return self._base_value - @base_value.setter - def base_value(self, v): - self._base_value = float(v) - @doc_inherit(SKCWeighterABC._weight_matrix) def _weight_matrix(self, matrix, **kwargs): return equal_weights(matrix, self.base_value) @@ -318,31 +314,24 @@ class Critic(SKCWeighterABC): } def __init__(self, correlation="pearson", scale=True): - self.correlation = correlation - self.scale = scale + correlation_func = self.CORRELATION.get(correlation, correlation) + if not callable(correlation_func): + corr_keys = ", ".join(f"'{c}'" for c in self.CORRELATION) + raise ValueError(f"Correlation must be {corr_keys} or callable") + self._correlation = correlation_func + + self._scale = bool(scale) @property def scale(self): """Return if it is necessary to scale the data.""" return self._scale - @scale.setter - def scale(self, v): - self._scale = bool(v) - @property def correlation(self): """Correlation function.""" return self._correlation - @correlation.setter - def correlation(self, v): - correlation_func = self.CORRELATION.get(v, v) - if not callable(correlation_func): - corr_keys = ", ".join(f"'{c}'" for c in self.CORRELATION) - raise ValueError(f"Correlation must be {corr_keys} or callable") - self._correlation = correlation_func - @doc_inherit(SKCWeighterABC._weight_matrix) def _weight_matrix(self, matrix, objectives, **kwargs): if Objective.MIN.value in objectives: diff --git a/skcriteria/utils/hmodel.py b/skcriteria/utils/hmodel.py deleted file mode 100644 index 408d979..0000000 --- a/skcriteria/utils/hmodel.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) -# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia -# Copyright (c) 2022, QuatroPe -# All rights reserved. - -# ============================================================================= -# DOCS -# ============================================================================= - -"""Framework for defining models with hyperparameters.""" - - -# ============================================================================= -# IMPORTS -# ============================================================================= - -from abc import ABCMeta - -import attr - - -# ============================================================================= -# CONSTANTS -# ============================================================================= - -HMODEL_METADATA_KEY = "__hmodel__" - -HMODEL_CONFIG = "__hmodel_config__" - -DEFAULT_HMODEL_CONFIG = {"repr": False, "frozen": False} - - -# ============================================================================= -# FUNCTIONS -# ============================================================================= - - -def hparam(default, **kwargs): - """Create a hyper parameter. - - By design decision, hyper-parameter is required to have a sensitive default - value. - - Parameters - ---------- - default : - Sensitive default value of the hyper-parameter. - **kwargs : - Additional keyword arguments are passed and are documented in - ``attr.ib()``. - - Return - ------ - Hyper parameter with a default value. - - Notes - ----- - This function is a thin-wrapper over the attrs function ``attr.ib()``. - """ - field_metadata = kwargs.pop("metadata", {}) - model_metadata = field_metadata.setdefault(HMODEL_METADATA_KEY, {}) - model_metadata["hparam"] = True - return attr.field( - default=default, metadata=field_metadata, kw_only=True, **kwargs - ) - - -def mproperty(**kwargs): - """Create a internal property for the model. - - By design decision, hyper-parameter is required to have a sensitive default - value. - - Parameters - ---------- - default : - Sensitive default value of the hyper-parameter. - **kwargs : - Additional keyword arguments are passed and are documented in - ``attr.ib()``. - - Return - ------ - Hyper parameter with a default value. - - Notes - ----- - This function is a thin-wrapper over the attrs function ``attr.ib()``. - - """ - field_metadata = kwargs.pop("metadata", {}) - model_metadata = field_metadata.setdefault(HMODEL_METADATA_KEY, {}) - model_metadata["mproperty"] = True - return attr.field(init=False, metadata=field_metadata, **kwargs) - - -@attr.define(repr=False) -class HModelABC(metaclass=ABCMeta): - """Create a new models with hyperparameters.""" - - def __init_subclass__(cls): - """Initiate of subclasses. - - It ensures that every inherited class is decorated by ``attr.define()`` - and assigns as class configuration the parameters defined in the class - variable `__hmodel_config__`. - - In other words it is slightly equivalent to: - - .. code-block:: python - - @attr.s(**HModelABC.__hmodel_config__) - class Decomposer(HModelABC): - pass - - """ - model_config = getattr(cls, HMODEL_CONFIG, DEFAULT_HMODEL_CONFIG) - return attr.define(maybe_cls=cls, slots=False, **model_config) - - @classmethod - def get_hparams(cls): - """Return a tuple of available hyper parameters.""" - - def flt(f): - return f.metadata[HMODEL_METADATA_KEY].get("hparam", False) - - return tuple(f.name for f in attr.fields(cls) if flt(f)) - - def __repr__(self): - """x.__repr__() <==> repr(x).""" - clsname = type(self).__name__ - - hparams = self.get_hparams() - selfd = attr.asdict( - self, - recurse=False, - filter=lambda attr, _: attr.name in hparams and attr.repr, - ) - - attrs_str = ", ".join( - [f"{k}={repr(v)}" for k, v in sorted(selfd.items())] - ) - return f"{clsname}({attrs_str})" diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index b0f78a3..466a930 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -92,7 +92,7 @@ def _transform_data(self, **kwargs): def test_not_redefined_SKCMatrixAndWeightTransformerMixin(): - class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): + class Foo(methods.SKCMatrixAndWeightTransformerABC): pass with pytest.raises(TypeError): @@ -106,7 +106,7 @@ class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): def test_bad_normalize_for_SKCMatrixAndWeightTransformerMixin(): - class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): + class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): ... @@ -120,7 +120,7 @@ def _transform_weights(self, weights): def test_transform_weights_not_implemented_SKCMatrixAndWeightTransformerMixin( decision_matrix, ): - class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): + class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): super()._transform_matrix(matrix) @@ -137,7 +137,7 @@ def _transform_weights(self, weights): def test_transform_weight_not_implemented_SKCMatrixAndWeightTransformerMixin( decision_matrix, ): - class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): + class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): return matrix @@ -152,7 +152,7 @@ def _transform_weights(self, weights): def test_SKCMatrixAndWeightTransformerMixin_target(): - class Foo(methods.SKCMatrixAndWeightTransformerABC, methods.SKCMethodABC): + class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): ... diff --git a/tests/utils/test_hmodel.py b/tests/utils/test_hmodel.py deleted file mode 100644 index 7ca1da5..0000000 --- a/tests/utils/test_hmodel.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) -# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia -# Copyright (c) 2022, QuatroPe -# All rights reserved. - -# ============================================================================= -# DOCS -# ============================================================================= - -"""test for skcriteria.utils.hmodel. - -""" - -# ============================================================================= -# IMPORTS -# ============================================================================= - - -import pytest - -from skcriteria.utils import hmodel - -# ============================================================================= -# tests -# ============================================================================= - - -def test_hparam(): - hp = hmodel.hparam(1) - - assert hp.metadata[hmodel.HMODEL_METADATA_KEY]["hparam"] - - -def test_mproperty(): - mp = hmodel.mproperty() - assert mp.metadata[hmodel.HMODEL_METADATA_KEY]["mproperty"] - - -def test_create_HModel(): - class MyBase(hmodel.HModelABC): - v = hmodel.hparam(42) - - class MyModel(MyBase): - - p = hmodel.hparam(25) - m = hmodel.mproperty() - - @m.default - def _md(self): - return self.p + 1 - - assert MyModel.get_hparams() == ("v", "p") - - model = MyModel() - assert model.v == 42 - assert model.p == 25 - assert model.m == 26 - - assert repr(model) == "MyModel(p=25, v=42)" - - model = MyModel(p=27, v=43) - assert model.v == 43 - assert model.p == 27 - assert model.m == 28 - assert repr(model) == "MyModel(p=27, v=43)" - - with pytest.raises(TypeError): - MyModel(27) - - with pytest.raises(TypeError): - MyModel(m=27) From d817d4f29a0f6d46fc3fa6fdbb5158e8c9cdf0da Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 26 Jan 2022 02:32:23 -0300 Subject: [PATCH 12/47] chasing a lot of bugs related to the parameters inspection --- skcriteria/core/methods.py | 39 ++++++++++++++--- skcriteria/madm/similarity.py | 27 ++---------- skcriteria/preprocessing/increment.py | 2 +- skcriteria/preprocessing/weighters.py | 2 +- tests/core/test_methods.py | 62 +++++++++++++++++++++++++-- 5 files changed, 98 insertions(+), 34 deletions(-) diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index 391f28b..feb2a4c 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -16,12 +16,12 @@ # =============================================================================ç import abc +import copy import inspect from .data import DecisionMatrix from ..utils import doc_inherit - # ============================================================================= # BASE DECISION MAKER CLASS # ============================================================================= @@ -55,10 +55,8 @@ def __init_subclass__(cls): if decisor_type is None: raise TypeError(f"{cls} must redefine '_skcriteria_dm_type'") - if ( - cls._skcriteria_parameters is None - and cls.__init__ is not SKCMethodABC.__init__ - ): + if "_skcriteria_parameters" not in vars(cls): + signature = inspect.signature(cls.__init__) parameters = set() for idx, param_tuple in enumerate(signature.parameters.items()): @@ -82,6 +80,37 @@ def __repr__(self): str_parameters = ", ".join(parameters) return f"{cls_name}({str_parameters})" + def get_parameters(self): + """Return the parameters of the method as dictionary.""" + the_parameters = {} + for parameter_name in self._skcriteria_parameters: + parameter_value = getattr(self, parameter_name) + the_parameters[parameter_name] = copy.deepcopy(parameter_value) + return the_parameters + + def copy(self, **kwargs): + """Return a deep copy of the current Object.. + + This method is also useful for manually modifying the values of the + object. + + Parameters + ---------- + kwargs : + The same parameters supported by object constructor. The values + provided replace the existing ones in the object to be copied. + + Returns + ------- + A new object. + + """ + asdict = self.get_parameters() + asdict.update(kwargs) + + cls = type(self) + return cls(**asdict) + # ============================================================================= # SKCTransformer MIXIN diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index 67fa5eb..62fdb01 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -115,28 +115,13 @@ class TOPSIS(SKCDecisionMakerABC): ``matching``, ``minkowski``, ``rogerstanimoto``, ``russellrao``, ``seuclidean``, ``sokalmichener``, ``sokalsneath``, ``sqeuclidean``, ``wminkowski``, ``yule``. - **cdist_kwargs : dict, optional - Extra arguments to metric: refer to each metric documentation for a - list of all possible arguments. - Some possible arguments: - - - p : scalar The p-norm to apply for Minkowski, weighted and - unweighted. Default: 2. - - w : array_like The weight vector for metrics that support weights - (e.g., Minkowski). - - V : array_like The variance vector for standardized Euclidean. - Default: var(vstack([XA, XB]), axis=0, ddof=1) - - VI : array_like The inverse of the covariance matrix for - Mahalanobis. Default: inv(cov(vstack([XA, XB].T))).T - - This extra parameters are passed to ``scipy.spatial.distance.cdist`` - function, Warnings -------- UserWarning: If some objective is to minimize. + References ---------- :cite:p:`hwang1981methods` @@ -145,25 +130,20 @@ class TOPSIS(SKCDecisionMakerABC): """ - def __init__(self, *, metric="euclidean", **cdist_kwargs): + def __init__(self, *, metric="euclidean"): + if not callable(metric) and metric not in _VALID_DISTANCES_METRICS: metrics = ", ".join(f"'{m}'" for m in _VALID_DISTANCES_METRICS) raise ValueError( f"Invalid metric '{metric}'. Plese choose from: {metrics}" ) self._metric = metric - self._cdist_kwargs = cdist_kwargs @property def metric(self): """Which distance metric will be used.""" return self._metric - @property - def cdist_kwargs(self): - """Extra parameters for ``scipy.spatial.distance.cdist()`` function.""" - return self._cdist_kwargs - @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): if Objective.MIN.value in objectives: @@ -177,7 +157,6 @@ def _evaluate_data(self, matrix, objectives, weights, **kwargs): objectives, weights, metric=self.metric, - **self.cdist_kwargs, ) return rank, { "ideal": ideal, diff --git a/skcriteria/preprocessing/increment.py b/skcriteria/preprocessing/increment.py index 27ad934..7118efd 100644 --- a/skcriteria/preprocessing/increment.py +++ b/skcriteria/preprocessing/increment.py @@ -87,7 +87,7 @@ class AddValueToZero(SKCMatrixAndWeightTransformerABC): """ - def __init__(self, value, target): + def __init__(self, target, value=1.0): super().__init__(target=target) self._eps = float(value) diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index f979783..93b7f4b 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -86,7 +86,7 @@ class EqualWeighter(SKCWeighterABC): """ - def __init__(self, base_value=1): + def __init__(self, base_value=1.0): self._base_value = float(base_value) @property diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 466a930..f35dcb9 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -24,12 +24,13 @@ from skcriteria.core import data, methods + # ============================================================================= # TESTS # ============================================================================= -def test_no__skcriteria_dm_type(): +def test_SKCMethodABC_no__skcriteria_dm_type(): with pytest.raises(TypeError): @@ -37,7 +38,7 @@ class Foo(methods.SKCMethodABC): pass -def test_repr(): +def test_SKCMethodABC_repr(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" @@ -50,7 +51,7 @@ def __init__(self, foo, faa): assert repr(foo) == "Foo(faa=1, foo=2)" -def test_repr_no_params(): +def test_SKCMethodABC_repr_no_params(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" @@ -59,6 +60,24 @@ class Foo(methods.SKCMethodABC): assert repr(foo) == "Foo()" +def test_SKCMethodABC_no_params(): + class Foo(methods.SKCMethodABC): + _skcriteria_dm_type = "foo" + + assert Foo._skcriteria_parameters == set() + + +def test_SKCMethodABC_alreadydefines__skcriteria_parameters(): + class Base(methods.SKCMethodABC): + _skcriteria_dm_type = "foo" + + class Foo(Base): + def __init__(self, x): + pass + + assert Foo._skcriteria_parameters == {"x"} + + # ============================================================================= # TRANSFORMER # ============================================================================= @@ -300,3 +319,40 @@ def _make_result(self, **kwargs): with pytest.raises(NotImplementedError): ranker.evaluate(dm) + + +# subclass testing + + +def _get_subclasses(cls): + + if ( + cls._skcriteria_dm_type not in (None, "pipeline") + and not cls.__abstractmethods__ + ): + yield cls + + for subc in cls.__subclasses__(): + for subsub in _get_subclasses(subc): + yield subsub + + +@pytest.mark.run(order=-1) +def test_SLCMethodABC_concrete_subclass_copy(): + + extra_parameters_by_type = { + methods.SKCMatrixAndWeightTransformerABC: {"target": "both"} + } + + for scls in _get_subclasses(methods.SKCMethodABC): + kwargs = {} + for cls, extra_params in extra_parameters_by_type.items(): + if issubclass(scls, cls): + kwargs.update(extra_params) + + original = scls(**kwargs) + copy = original.copy() + + assert ( + original.get_parameters() == copy.get_parameters() + ), f"'{scls.__qualname__}' instance not correctly copied." From 39e9854f5bf79cf4bc9719f9146b45da426ff832 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 26 Jan 2022 02:48:31 -0300 Subject: [PATCH 13/47] now The automatic parameter inspection does not work with variable parameters. --- skcriteria/core/methods.py | 25 +++++++++++++++++-------- tests/core/test_methods.py | 11 +++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index feb2a4c..9775651 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -27,7 +27,7 @@ # ============================================================================= -_IGNORE_PARAMS = ( +_INVALID_FOR_AUTO_PARAMS_INSPECTION = ( inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD, ) @@ -56,14 +56,23 @@ def __init_subclass__(cls): raise TypeError(f"{cls} must redefine '_skcriteria_dm_type'") if "_skcriteria_parameters" not in vars(cls): - - signature = inspect.signature(cls.__init__) parameters = set() - for idx, param_tuple in enumerate(signature.parameters.items()): - if idx == 0: # first arugment of a method is the instance - continue - name, param = param_tuple - if param.kind not in _IGNORE_PARAMS: + + if cls.__init__ is not SKCMethodABC.__init__: + signature = inspect.signature(cls.__init__) + + for idx, param_tuple in enumerate( + signature.parameters.items() + ): + if idx == 0: # first arugment of a method is the instance + continue + name, param = param_tuple + if param.kind in _INVALID_FOR_AUTO_PARAMS_INSPECTION: + raise TypeError( + "The automatic parameter inspection " + "does not work with variable parameters." + ) + parameters.add(name) cls._skcriteria_parameters = frozenset(parameters) diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index f35dcb9..45c6299 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -38,6 +38,17 @@ class Foo(methods.SKCMethodABC): pass +def test_SKCMethodABC_varargs_for__skcriteria_parameters_inspection(): + + with pytest.raises(TypeError): + + class Foo(methods.SKCMethodABC): + _skcriteria_dm_type = "foo" + + def __init__(self, **kwargs): + pass + + def test_SKCMethodABC_repr(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" From b064d690ab6b27752da2c0e542389d06bd6e03d7 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 26 Jan 2022 17:00:52 -0300 Subject: [PATCH 14/47] bump 0.6dev0 --- skcriteria/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skcriteria/__init__.py b/skcriteria/__init__.py index 16e7540..73b78ba 100644 --- a/skcriteria/__init__.py +++ b/skcriteria/__init__.py @@ -30,7 +30,7 @@ __all__ = ["mkdm", "DecisionMatrix", "Objective"] -__version__ = ("0", "5") +__version__ = ("0", "6dev0") NAME = "scikit-criteria" From c2b4a6da3654406d065f26eeee8d9949d9fff1f9 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 26 Jan 2022 17:02:42 -0300 Subject: [PATCH 15/47] feat: Deprecation mechanism --- setup.py | 1 + skcriteria/utils/__init__.py | 4 +-- skcriteria/utils/decorators.py | 47 ++++++++++++++++++++++++++++++++++ tests/utils/test_decorators.py | 38 +++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5d12374..cba8f19 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "custom_inherit", "seaborn", "pulp", + "Deprecated", ] PATH = pathlib.Path(os.path.abspath(os.path.dirname(__file__))) diff --git a/skcriteria/utils/__init__.py b/skcriteria/utils/__init__.py index d479c10..a946591 100644 --- a/skcriteria/utils/__init__.py +++ b/skcriteria/utils/__init__.py @@ -17,10 +17,10 @@ from . import lp, rank from .bunch import Bunch -from .decorators import doc_inherit +from .decorators import deprecated, doc_inherit # ============================================================================= # ALL # ============================================================================= -__all__ = ["doc_inherit", "rank", "Bunch", "lp", "dominance"] +__all__ = ["doc_inherit", "deprecated", "rank", "Bunch", "lp", "dominance"] diff --git a/skcriteria/utils/decorators.py b/skcriteria/utils/decorators.py index d64f967..7aa58b9 100644 --- a/skcriteria/utils/decorators.py +++ b/skcriteria/utils/decorators.py @@ -16,6 +16,8 @@ # ============================================================================= from custom_inherit import doc_inherit as _doc_inherit +from deprecated import deprecated as _deprecated + # ============================================================================= # DOC INHERITANCE # ============================================================================= @@ -44,3 +46,48 @@ def doc_inherit(parent): """ return _doc_inherit(parent, style="numpy") + + +# ============================================================================= +# Deprecation +# ============================================================================= + + +class SKCriteriaDeprecationWarning(DeprecationWarning): + """Skcriteria deprecation warning.""" + + +# _ If the version of the warning is >= ERROR_GE the action is setted to +# 'error', otherwise is 'once'. +ERROR_GE = 1.0 + + +def deprecated(*, reason, version): + """Mark functions, classes and methods as deprecated. + + It will result in a warning being emitted when the object is called, + and the "deprecated" directive was added to the docstring. + + Parameters + ---------- + reason: str + Reason message which documents the deprecation in your library. + version: str + Version of your project which deprecates this feature. + If you follow the `Semantic Versioning `_, + the version number has the format "MAJOR.MINOR.PATCH". + + Notes + ----- + This decorator is a thin layer over + :py:func:`deprecated.deprecated`. + + Check: __ + + """ + return _deprecated( + reason=reason, + version=version, + category=SKCriteriaDeprecationWarning, + action=("error" if version >= ERROR_GE else "once"), + ) diff --git a/tests/utils/test_decorators.py b/tests/utils/test_decorators.py index f51ea11..d597a18 100644 --- a/tests/utils/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -22,6 +22,8 @@ import numpy as np +import pytest + from skcriteria.utils import decorators @@ -51,3 +53,39 @@ def func_c(): ... assert doc == func_a.__doc__ == func_b.__doc__ == func_c.__doc__ + + +def test_deprecated(): + def func(): + """Zaraza. + + Foo + + Parameters + ---------- + a: int + coso. + + Returns + ------- + None: + Nothing to return. + + """ + pass + + expected_doc = func.__doc__ + + decorator = decorators.deprecated(reason="because foo", version=0.66) + func = decorator(func) + + with pytest.deprecated_call(): + func() + + assert func.__doc__ == expected_doc + + # this can be useful to catch bugs + # print("-" * 100) + # print(repr(func.__doc__)) + # print(repr(expected_doc)) + # print("-" * 100) From 8f1b09b3ce8c1446163ad8cedbf194218d14378c Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 26 Jan 2022 21:11:02 -0300 Subject: [PATCH 16/47] feat: Stats accessor --- skcriteria/core/data.py | 110 +++++++++++++++++++++++++++++++++++- tests/core/test_data.py | 122 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 230 insertions(+), 2 deletions(-) diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 09cf1a8..faf4bf2 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -31,8 +31,9 @@ import pyquery as pq + from .plot import DecisionMatrixPlotter -from ..utils import Bunch, doc_inherit +from ..utils import Bunch, deprecated, doc_inherit # ============================================================================= @@ -118,6 +119,100 @@ def to_string(self): # ============================================================================= +class DecisionMatrixStatsAccessor: + """Calculate basic statistics of the decision matrix.""" + + _DF_WHITELIST = ( + "corr", + "cov", + "describe", + "kurtosis", + "mad", + "max", + "mean", + "median", + "min", + "pct_change", + "quantile", + "sem", + "skew", + "std", + "var", + ) + + _DEFAULT_KIND = "describe" + + def __init__(self, dm): + self._dm = dm + + def __call__(self, kind=None, **kwargs): + """Calculate basic statistics of the decision matrix. + + Parameters + ---------- + kind : str + The kind of statistic to produce: + + - 'corr' : Compute pairwise correlation of columns, excluding + NA/null values. + - 'cov' : Compute pairwise covariance of columns, excluding NA/null + values. + - 'describe' : Generate descriptive statistics. + - 'kurtosis' : Return unbiased kurtosis over requested axis. + - 'mad' : Return the mean absolute deviation of the values over the + requested axis. + - 'max' : Return the maximum of the values over the requested axis. + - 'mean' : Return the mean of the values over the requested axis. + - 'median' : Return the median of the values over the requested + axis. + - 'min' : Return the minimum of the values over the requested axis. + - 'pct_change' : Percentage change between the current and a prior + element. + - 'quantile' : Return values at the given quantile over requested + axis. + - 'sem' : Return unbiased standard error of the mean over requested + axis. + - 'skew' : Return unbiased skew over requested axis. + - 'std' : Return sample standard deviation over requested axis. + - 'var' : Return unbiased variance over requested axis. + + **kwargs + Options to pass to subjacent DataFrame method. + + Returns + ------- + :class:`matplotlib.axes.Axes` or numpy.ndarray of them + The ax used by the plot + + """ + kind = self._DEFAULT_KIND if kind is None else kind + + if kind.startswith("_"): + raise ValueError(f"invalid kind name '{kind}'") + + method = getattr(self, kind, None) + if not callable(method): + raise ValueError(f"Invalid kind name '{kind}'") + + return method(**kwargs) + + def __repr__(self): + """x.__repr__() <==> repr(x).""" + return f"{type(self).__name__}({self._dm})" + + def __getattr__(self, a): + """x.__getattr__(a) <==> x.a <==> getattr(x, "a").""" + if a not in self._DF_WHITELIST: + raise AttributeError(a) + return getattr(self._dm._data_df, a) + + def __dir__(self): + """x.__dir__() <==> dir(x).""" + return super().__dir__() + [ + e for e in dir(self._dm._data_df) if e in self._DF_WHITELIST + ] + + class DecisionMatrix: """Representation of all data needed in the MCDA analysis. @@ -395,6 +490,11 @@ def plot(self): """Plot accessor.""" return DecisionMatrixPlotter(self) + @property + def stats(self): + """Descriptive statistics accessor.""" + return DecisionMatrixStatsAccessor(self) + # UTILITIES =============================================================== def copy(self, **kwargs): @@ -470,6 +570,14 @@ def to_dict(self): "criteria": self.criteria, } + @deprecated( + reason=( + "Use 'DecisionMatrix.stats()', " + "'DecisionMatrix.stats(\"describe\")' or " + "'DecisionMatrix.stats.describe()' instead." + ), + version=0.6, + ) def describe(self, **kwargs): """Generate descriptive statistics. diff --git a/tests/core/test_data.py b/tests/core/test_data.py index ff3e182..47f9cd8 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -68,6 +68,110 @@ def test_objective_to_string(): assert data.Objective.MIN.to_string() == data.Objective._MIN_STR.value +# ============================================================================= +# STATS +# ============================================================================= + + +def test_DecisionMatrixStatsAccessor_default_kind(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + assert stats().equals(dm._data_df.describe()) + + +@pytest.mark.parametrize( + "kind", data.DecisionMatrixStatsAccessor._DF_WHITELIST +) +def test_DecisionMatrixStatsAccessor_df_whitelist_by_kind( + kind, decision_matrix +): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + expected = getattr(dm._data_df, kind)() + + stats = data.DecisionMatrixStatsAccessor(dm) + + result_call = stats(kind=kind) + + cmp = ( + lambda r, e: r.equals(e) + if isinstance(result_call, (pd.DataFrame, pd.Series)) + else np.equal + ) + + result_method = getattr(stats, kind)() + + assert cmp(result_call, expected) + assert cmp(result_method, expected) + + +def test_DecisionMatrixStatsAccessor_invalid_kind(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + with pytest.raises(ValueError): + stats("_dm") + + stats.foo = None + with pytest.raises(ValueError): + stats("foo") + + with pytest.raises(AttributeError): + stats.to_csv() + + +def test_DecisionMatrixStatsAccessor_repr(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + assert repr(stats) == f"DecisionMatrixStatsAccessor({repr(dm)})" + + +def test_DecisionMatrixStatsAccessor_dir(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + expected = set(data.DecisionMatrixStatsAccessor._DF_WHITELIST) + result = dir(stats) + + assert not expected.difference(result) + + # ============================================================================= # MUST WORK # ============================================================================= @@ -215,6 +319,21 @@ def test_DecisionMatrix_plot(data_values): assert dm.plot._dm is dm +def test_DecisionMatrix_stats(data_values): + mtx, objectives, weights, alternatives, criteria = data_values(seed=42) + + dm = data.mkdm( + matrix=mtx, + objectives=objectives, + weights=weights, + alternatives=alternatives, + criteria=criteria, + ) + + assert isinstance(dm.stats, data.DecisionMatrixStatsAccessor) + assert dm.stats._dm is dm + + # ============================================================================= # DECISION MATRIX # ============================================================================= @@ -301,7 +420,8 @@ def test_DecisionMatrix_describe(data_values): mtx, columns=criteria, index=alternatives ).describe() - result = dm.describe() + with pytest.deprecated_call(): + result = dm.describe() assert result.equals(expected) From 1be67b9bae35402d0a6a2afb0b77802bd2572904 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 28 Jan 2022 21:50:24 -0300 Subject: [PATCH 17/47] filter prototype --- skcriteria/core/methods.py | 12 +- skcriteria/filters.ipynb | 413 +++++++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+), 5 deletions(-) create mode 100644 skcriteria/filters.ipynb diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index 9775651..a54255f 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -242,16 +242,18 @@ def _transform_matrix(self, matrix): @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, weights, **kwargs): - norm_mtx = matrix - norm_weights = weights + transformed_mtx = matrix + transformed_weights = weights if self._target in (self._TARGET_MATRIX, self._TARGET_BOTH): - norm_mtx = self._transform_matrix(matrix) + transformed_mtx = self._transform_matrix(matrix) if self._target in (self._TARGET_WEIGHTS, self._TARGET_BOTH): - norm_weights = self._transform_weights(weights) + transformed_weights = self._transform_weights(weights) - kwargs.update(matrix=norm_mtx, weights=norm_weights, dtypes=None) + kwargs.update( + matrix=transformed_mtx, weights=transformed_weights, dtypes=None + ) return kwargs diff --git a/skcriteria/filters.ipynb b/skcriteria/filters.ipynb new file mode 100644 index 0000000..2c17d42 --- /dev/null +++ b/skcriteria/filters.ipynb @@ -0,0 +1,413 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import skcriteria as skc\n", + "from skcriteria.core.methods import SKCTransformerABC\n", + "import numpy as np\n", + "import abc\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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", + " ROE[▲ 2.0]CAP[▲ 4.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
MM1730
FN5830
\n", + "
5 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "JN 5 4 26\n", + "AA 5 6 28\n", + "MM 1 7 30\n", + "FN 5 8 30\n", + "[5 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dm = skc.mkdm(\n", + " matrix=[[7, 5, 35], [5, 4, 26], [5, 6, 28], [1, 7, 30], [5, 8, 30]],\n", + " objectives=[max, max, min],\n", + " weights=[2, 4, 1],\n", + " alternatives=[\"PE\", \"JN\", \"AA\", \"MM\", \"FN\"],\n", + " criteria=[\"ROE\", \"CAP\", \"RI\"],\n", + ")\n", + "\n", + "dm" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "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", + " C0[▲ 2.0]C1[▲ 4.0]C2[▼ 1.0]
PE7535
AA5628
FN5830
\n", + "
3 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " C0[▲ 2.0] C1[▲ 4.0] C2[▼ 1.0]\n", + "PE 7 5 35\n", + "AA 5 6 28\n", + "FN 5 8 30\n", + "[3 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class SKCFilterABC(SKCTransformerABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + "\n", + " def __init__(self, **criteria_filters):\n", + " if not criteria_filters:\n", + " raise ValueError()\n", + " self._criteria_filters = criteria_filters\n", + "\n", + " @property\n", + " def criteria_filters(self):\n", + " return dict(self._criteria_filters)\n", + "\n", + " @abc.abstractmethod\n", + " def _make_mask(self, matrix, criteria):\n", + " raise NotImplementedError()\n", + "\n", + " def _transform_data(self, matrix, criteria, alternatives, **kwargs):\n", + " criteria_not_found = set(self._criteria_filters).difference(criteria)\n", + " if criteria_not_found:\n", + " raise ValueError(f\"Missing criteria: {criteria_not_found}\")\n", + "\n", + " mask = self._make_mask(matrix, criteria)\n", + "\n", + " filtered_matrix = matrix[mask]\n", + " filtered_alternatives = alternatives[mask]\n", + "\n", + " kwargs.update(\n", + " matrix=filtered_matrix,\n", + " alternatives=filtered_alternatives,\n", + " dtypes=None,\n", + " )\n", + " return kwargs\n", + "\n", + "\n", + "class SKCFilterOperatorABC(SKCFilterABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + "\n", + " @property\n", + " @abc.abstractmethod\n", + " def _filter(self, arr, cond):\n", + " raise NotImplementedError()\n", + "\n", + " def _make_mask(self, matrix, criteria):\n", + " cnames, climits = [], []\n", + " for cname, climit in self._criteria_filters.items():\n", + " cnames.append(cname)\n", + " climits.append(climit)\n", + "\n", + " idxs = np.in1d(criteria, cnames)\n", + " matrix = matrix[:, idxs]\n", + " mask = np.all(self._filter(matrix, climits), axis=1)\n", + "\n", + " return mask\n", + "\n", + "\n", + "class FilterGT(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.greater\n", + "\n", + "\n", + "class FilterGE(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.greater_equal\n", + "\n", + "\n", + "class FilterLT(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.less\n", + "\n", + "\n", + "class FilterLE(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.less_equal\n", + "\n", + "\n", + "class FilterEQ(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.equal\n", + "\n", + "\n", + "class FilterNE(SKCFilterOperatorABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + " _filter = np.not_equal\n", + "\n", + "\n", + "class Filter(SKCFilterABC):\n", + "\n", + " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", + "\n", + " def _make_mask(self, matrix, criteria):\n", + " mask_list = []\n", + " for cname, flt_func in self._criteria_filters.items():\n", + " crit_idx = np.in1d(criteria, cname, assume_unique=False)\n", + " crit_array = matrix[:, crit_idx].flatten()\n", + " crit_mask = np.apply_along_axis(flt_func, axis=0, arr=crit_array)\n", + " mask_list.append(crit_mask)\n", + " \n", + " mask = np.all(np.column_stack(mask_list), axis=1)\n", + "\n", + " return mask\n", + "\n", + "\n", + "Filter(ROE=lambda e: e > 1, RI=lambda e: e >= 28).transform(dm)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "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", + " ROE[▲ 2.0]CAP[▲ 4.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
MM1730
FN5830
\n", + "
5 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "JN 5 4 26\n", + "AA 5 6 28\n", + "MM 1 7 30\n", + "FN 5 8 30\n", + "[5 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "0a948a28a0ef94db8982fe8442467cdefb8d68ae66a4b8f3824263fcc702cc89" + }, + "kernelspec": { + "display_name": "Python 3.9.10 64-bit ('skcriteria': virtualenv)", + "language": "python", + "name": "python3" + }, + "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.10" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 3812a3944e6869b594aa50b95a7d658d27c762bf Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 13:28:08 -0300 Subject: [PATCH 18/47] mmanual _skcriteria_parameters --- skcriteria/core/methods.py | 51 ++++++++----------- skcriteria/madm/electre.py | 1 + skcriteria/madm/moora.py | 4 ++ skcriteria/madm/similarity.py | 1 + skcriteria/madm/simple.py | 3 ++ skcriteria/madm/simus.py | 1 + skcriteria/preprocessing/distance.py | 1 + skcriteria/preprocessing/increment.py | 1 + skcriteria/preprocessing/invert_objectives.py | 1 + skcriteria/preprocessing/weighters.py | 5 ++ tests/core/test_methods.py | 8 ++- 11 files changed, 45 insertions(+), 32 deletions(-) diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index a54255f..58a875f 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -27,12 +27,6 @@ # ============================================================================= -_INVALID_FOR_AUTO_PARAMS_INSPECTION = ( - inspect.Parameter.VAR_POSITIONAL, - inspect.Parameter.VAR_KEYWORD, -) - - class SKCMethodABC(metaclass=abc.ABCMeta): """Base class for all class in scikit-criteria. @@ -41,40 +35,29 @@ class SKCMethodABC(metaclass=abc.ABCMeta): All estimators should specify: - ``_skcriteria_dm_type``: The type of the decision maker. + - ``_skcriteria_parameters``: Availebe parameters. + - ``_skcriteria_abstract_method``: If the class is abstract (this attribute + is not inheritable) + If the class is *abstract* the user can ignore the other two attributes. """ - _skcriteria_dm_type = None - _skcriteria_parameters = None - def __init_subclass__(cls): """Validate if the subclass are well formed.""" - decisor_type = cls._skcriteria_dm_type + is_abstract = vars(cls).get("_skcriteria_abstract_method", False) + if is_abstract: + return + decisor_type = getattr(cls, "_skcriteria_dm_type", None) if decisor_type is None: raise TypeError(f"{cls} must redefine '_skcriteria_dm_type'") + cls._skcriteria_dm_type = str(decisor_type) - if "_skcriteria_parameters" not in vars(cls): - parameters = set() - - if cls.__init__ is not SKCMethodABC.__init__: - signature = inspect.signature(cls.__init__) - - for idx, param_tuple in enumerate( - signature.parameters.items() - ): - if idx == 0: # first arugment of a method is the instance - continue - name, param = param_tuple - if param.kind in _INVALID_FOR_AUTO_PARAMS_INSPECTION: - raise TypeError( - "The automatic parameter inspection " - "does not work with variable parameters." - ) - - parameters.add(name) - cls._skcriteria_parameters = frozenset(parameters) + params = getattr(cls, "_skcriteria_parameters", None) + if params is None: + raise TypeError(f"{cls} must redefine '_skcriteria_parameters'") + cls._skcriteria_parameters = frozenset(params) def __repr__(self): """x.__repr__() <==> repr(x).""" @@ -130,6 +113,7 @@ class SKCTransformerABC(SKCMethodABC): """Mixin class for all transformer in scikit-criteria.""" _skcriteria_dm_type = "transformer" + _skcriteria_abstract_method = True @abc.abstractmethod def _transform_data(self, **kwargs): @@ -184,6 +168,9 @@ class SKCMatrixAndWeightTransformerABC(SKCTransformerABC): """ + _skcriteria_abstract_method = True + _skcriteria_parameters = ["target"] + _TARGET_WEIGHTS = "weights" _TARGET_MATRIX = "matrix" _TARGET_BOTH = "both" @@ -271,6 +258,8 @@ class SKCWeighterABC(SKCTransformerABC): """ + _skcriteria_abstract_method = True + @abc.abstractmethod def _weight_matrix(self, matrix, objectives, weights): """Calculate a new array of weights. @@ -314,6 +303,8 @@ def _transform_data(self, matrix, objectives, weights, **kwargs): class SKCDecisionMakerABC(SKCMethodABC): """Mixin class for all decisor based methods in scikit-criteria.""" + _skcriteria_abstract_method = True + _skcriteria_dm_type = "decision_maker" @abc.abstractmethod diff --git a/skcriteria/madm/electre.py b/skcriteria/madm/electre.py index 0ac15d0..f8225ba 100644 --- a/skcriteria/madm/electre.py +++ b/skcriteria/madm/electre.py @@ -143,6 +143,7 @@ class ELECTRE1(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = ["p", "q"] def __init__(self, p=0.65, q=0.35): self._p = float(p) diff --git a/skcriteria/madm/moora.py b/skcriteria/madm/moora.py index 930d5dc..98dbc05 100644 --- a/skcriteria/madm/moora.py +++ b/skcriteria/madm/moora.py @@ -68,6 +68,7 @@ class RatioMOORA(SKCDecisionMakerABC): :cite:p:`brauers2006moora` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): @@ -131,6 +132,7 @@ class ReferencePointMOORA(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): @@ -217,6 +219,7 @@ class FullMultiplicativeForm(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): @@ -303,6 +306,7 @@ class MultiMOORA(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index 62fdb01..761baa7 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -129,6 +129,7 @@ class TOPSIS(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = ["metric"] def __init__(self, *, metric="euclidean"): diff --git a/skcriteria/madm/simple.py b/skcriteria/madm/simple.py index 8d57e2d..c28ce23 100644 --- a/skcriteria/madm/simple.py +++ b/skcriteria/madm/simple.py @@ -74,6 +74,7 @@ class WeightedSumModel(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, weights, objectives, **kwargs): @@ -162,6 +163,8 @@ class WeightedProductModel(SKCDecisionMakerABC): """ + _skcriteria_parameters = [] + @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, weights, objectives, **kwargs): if Objective.MIN.value in objectives: diff --git a/skcriteria/madm/simus.py b/skcriteria/madm/simus.py index 94aaad0..0ce91a2 100644 --- a/skcriteria/madm/simus.py +++ b/skcriteria/madm/simus.py @@ -246,6 +246,7 @@ class SIMUS(SKCDecisionMakerABC): `PuLP Documentation `_ """ + _skcriteria_parameters = ["rank_by", "solver"] def __init__(self, *, rank_by=1, solver="pulp"): if not ( diff --git a/skcriteria/preprocessing/distance.py b/skcriteria/preprocessing/distance.py index d7c0319..e40047c 100644 --- a/skcriteria/preprocessing/distance.py +++ b/skcriteria/preprocessing/distance.py @@ -82,6 +82,7 @@ class CenitDistance(SKCTransformerABC): :cite:p:`diakoulaki1995determining` """ + _skcriteria_parameters = [] @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, objectives, **kwargs): diff --git a/skcriteria/preprocessing/increment.py b/skcriteria/preprocessing/increment.py index 7118efd..f91997f 100644 --- a/skcriteria/preprocessing/increment.py +++ b/skcriteria/preprocessing/increment.py @@ -86,6 +86,7 @@ class AddValueToZero(SKCMatrixAndWeightTransformerABC): \overline{X}_{ij} = X_{ij} + value """ + _skcriteria_parameters = ["target", "value"] def __init__(self, target, value=1.0): super().__init__(target=target) diff --git a/skcriteria/preprocessing/invert_objectives.py b/skcriteria/preprocessing/invert_objectives.py index 0885dbb..3ed75b1 100644 --- a/skcriteria/preprocessing/invert_objectives.py +++ b/skcriteria/preprocessing/invert_objectives.py @@ -93,6 +93,7 @@ class MinimizeToMaximize(SKCTransformerABC): ones thar are converted to ``numpy.float64``. """ + _skcriteria_parameters = [] @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, objectives, dtypes, **kwargs): diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index 93b7f4b..0019927 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -85,6 +85,7 @@ class EqualWeighter(SKCWeighterABC): total criteria. """ + _skcriteria_parameters = ["base_value"] def __init__(self, base_value=1.0): self._base_value = float(base_value) @@ -142,6 +143,7 @@ def std_weights(matrix): class StdWeighter(SKCWeighterABC): """Set as weight the normalized standard deviation of each criterion.""" + _skcriteria_parameters = [] @doc_inherit(SKCWeighterABC._weight_matrix) def _weight_matrix(self, matrix, **kwargs): @@ -193,6 +195,7 @@ class EntropyWeighter(SKCWeighterABC): Calculate the entropy of a distribution for given probability values. """ + _skcriteria_parameters = [] @doc_inherit(SKCWeighterABC._weight_matrix) def _weight_matrix(self, matrix, **kwargs): @@ -313,6 +316,8 @@ class Critic(SKCWeighterABC): "spearman": spearman_correlation, } + _skcriteria_parameters = ["correlation", "scale"] + def __init__(self, correlation="pearson", scale=True): correlation_func = self.CORRELATION.get(correlation, correlation) if not callable(correlation_func): diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 45c6299..c702265 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -38,7 +38,7 @@ class Foo(methods.SKCMethodABC): pass -def test_SKCMethodABC_varargs_for__skcriteria_parameters_inspection(): +def test_SKCMethodABC_no__skcriteria_parameters(): with pytest.raises(TypeError): @@ -52,6 +52,7 @@ def __init__(self, **kwargs): def test_SKCMethodABC_repr(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" + _skcriteria_parameters = ["foo", "faa"] def __init__(self, foo, faa): self.foo = foo @@ -65,6 +66,7 @@ def __init__(self, foo, faa): def test_SKCMethodABC_repr_no_params(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" + skcriteria_parameters = [] foo = Foo() @@ -74,13 +76,15 @@ class Foo(methods.SKCMethodABC): def test_SKCMethodABC_no_params(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" + skcriteria_parameters = [] - assert Foo._skcriteria_parameters == set() + assert Foo._skcriteria_parameters == frozenset() def test_SKCMethodABC_alreadydefines__skcriteria_parameters(): class Base(methods.SKCMethodABC): _skcriteria_dm_type = "foo" + skcriteria_parameters = ["x"] class Foo(Base): def __init__(self, x): From 1e930701f5731aac6bee8113ad619c31896cf95a Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 14:34:49 -0300 Subject: [PATCH 19/47] all tests passes --- skcriteria/pipeline.py | 2 ++ tests/core/test_methods.py | 33 ++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index c1b57cd..f3f20a6 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -54,6 +54,8 @@ class SKCPipeline(SKCMethodABC): """ _skcriteria_dm_type = "pipeline" + _skcriteria_parameters = ["steps"] + def __init__(self, steps): self._validate_steps(steps) diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index c702265..bf75d2e 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -66,7 +66,7 @@ def __init__(self, foo, faa): def test_SKCMethodABC_repr_no_params(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" - skcriteria_parameters = [] + _skcriteria_parameters = [] foo = Foo() @@ -76,15 +76,15 @@ class Foo(methods.SKCMethodABC): def test_SKCMethodABC_no_params(): class Foo(methods.SKCMethodABC): _skcriteria_dm_type = "foo" - skcriteria_parameters = [] + _skcriteria_parameters = [] assert Foo._skcriteria_parameters == frozenset() -def test_SKCMethodABC_alreadydefines__skcriteria_parameters(): +def test_SKCMethodABC_already_defined__skcriteria_parameters(): class Base(methods.SKCMethodABC): _skcriteria_dm_type = "foo" - skcriteria_parameters = ["x"] + _skcriteria_parameters = ["x"] class Foo(Base): def __init__(self, x): @@ -100,7 +100,7 @@ def __init__(self, x): def test_not_redefined_SKCTransformerMixin(): class Foo(methods.SKCTransformerABC): - pass + _skcriteria_parameters = [] with pytest.raises(TypeError): Foo() @@ -115,6 +115,8 @@ def test_transform_data_not_implemented_SKCMatrixAndWeightTransformerMixin( decision_matrix, ): class Foo(methods.SKCTransformerABC): + _skcriteria_parameters = [] + def _transform_data(self, **kwargs): return super()._transform_data(**kwargs) @@ -213,6 +215,8 @@ def _transform_weights(self, weights): def test_weight_matrix_not_implemented_SKCWeighterMixin(decision_matrix): class Foo(methods.SKCWeighterABC): + _skcriteria_parameters = [] + def _weight_matrix(self, **kwargs): return super()._weight_matrix(**kwargs) @@ -225,7 +229,7 @@ def _weight_matrix(self, **kwargs): def test_not_redefined_SKCWeighterMixin(): class Foo(methods.SKCWeighterABC): - pass + _skcriteria_parameters = [] with pytest.raises(TypeError): Foo() @@ -237,6 +241,8 @@ def test_flow_SKCWeighterMixin(decision_matrix): expected_weights = np.ones(dm.matrix.shape[1]) * 42 class Foo(methods.SKCWeighterABC): + _skcriteria_parameters = [] + def _weight_matrix(self, matrix, **kwargs): return expected_weights @@ -266,6 +272,8 @@ def test_flow_SKCDecisionMakerMixin(decision_matrix): dm = decision_matrix(seed=42) class Foo(methods.SKCDecisionMakerABC): + _skcriteria_parameters = ["x"] + def _evaluate_data(self, alternatives, **kwargs): return np.arange(len(alternatives)) + 1, {} @@ -287,7 +295,7 @@ def _make_result(self, alternatives, values, extra): @pytest.mark.parametrize("not_redefine", ["_evaluate_data", "_make_result"]) def test_not_redefined_SKCDecisionMakerMixin(not_redefine): - content = {} + content = {"_skcriteria_parameters": ["x"]} for method_name in ["_evaluate_data", "_make_result", "_validate_data"]: if method_name != not_redefine: content[method_name] = lambda **kws: None @@ -303,6 +311,8 @@ def test_evaluate_data_not_implemented_SKCDecisionMakerMixin(decision_matrix): dm = decision_matrix(seed=42) class Foo(methods.SKCDecisionMakerABC): + _skcriteria_parameters = [] + def _evaluate_data(self, **kwargs): super()._evaluate_data(**kwargs) @@ -324,6 +334,8 @@ def test_make_result_not_implemented_SKCDecisionMakerMixin(decision_matrix): dm = decision_matrix(seed=42) class Foo(methods.SKCDecisionMakerABC): + _skcriteria_parameters = [] + def _evaluate_data(self, alternatives, **kwargs): return np.arange(len(alternatives)) + 1, {} @@ -341,10 +353,9 @@ def _make_result(self, **kwargs): def _get_subclasses(cls): - if ( - cls._skcriteria_dm_type not in (None, "pipeline") - and not cls.__abstractmethods__ - ): + dmtype = getattr(cls, "_skcriteria_dm_type", None) + + if dmtype not in (None, "pipeline") and not cls.__abstractmethods__: yield cls for subc in cls.__subclasses__(): From 63158310045e74a922ba2c6af648bea6cf1f3353 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 15:22:41 -0300 Subject: [PATCH 20/47] manual parameters updated and enforced --- skcriteria/core/methods.py | 28 +++++++++---- skcriteria/filters.ipynb | 6 +-- skcriteria/madm/electre.py | 1 + skcriteria/madm/similarity.py | 1 + skcriteria/pipeline.py | 1 - skcriteria/preprocessing/weighters.py | 3 ++ tests/core/test_methods.py | 58 ++++++++++++++++++--------- 7 files changed, 66 insertions(+), 32 deletions(-) diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index 58a875f..de11acc 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -36,16 +36,17 @@ class SKCMethodABC(metaclass=abc.ABCMeta): - ``_skcriteria_dm_type``: The type of the decision maker. - ``_skcriteria_parameters``: Availebe parameters. - - ``_skcriteria_abstract_method``: If the class is abstract (this attribute - is not inheritable) + - ``_skcriteria_abstract_class``: If the class is abstract. If the class is *abstract* the user can ignore the other two attributes. """ + _skcriteria_abstract_class = True + def __init_subclass__(cls): """Validate if the subclass are well formed.""" - is_abstract = vars(cls).get("_skcriteria_abstract_method", False) + is_abstract = vars(cls).get("_skcriteria_abstract_class", False) if is_abstract: return @@ -57,7 +58,18 @@ def __init_subclass__(cls): params = getattr(cls, "_skcriteria_parameters", None) if params is None: raise TypeError(f"{cls} must redefine '_skcriteria_parameters'") - cls._skcriteria_parameters = frozenset(params) + + params = frozenset(params) + + signature = inspect.signature(cls.__init__) + params_not_in_signature = params.difference(signature.parameters) + if params_not_in_signature: + raise TypeError( + f"{cls} defines the parameters {params_not_in_signature} " + "which is not found as a parameter in the __init__ method." + ) + + cls._skcriteria_parameters = params def __repr__(self): """x.__repr__() <==> repr(x).""" @@ -113,7 +125,7 @@ class SKCTransformerABC(SKCMethodABC): """Mixin class for all transformer in scikit-criteria.""" _skcriteria_dm_type = "transformer" - _skcriteria_abstract_method = True + _skcriteria_abstract_class = True @abc.abstractmethod def _transform_data(self, **kwargs): @@ -168,7 +180,7 @@ class SKCMatrixAndWeightTransformerABC(SKCTransformerABC): """ - _skcriteria_abstract_method = True + _skcriteria_abstract_class = True _skcriteria_parameters = ["target"] _TARGET_WEIGHTS = "weights" @@ -258,7 +270,7 @@ class SKCWeighterABC(SKCTransformerABC): """ - _skcriteria_abstract_method = True + _skcriteria_abstract_class = True @abc.abstractmethod def _weight_matrix(self, matrix, objectives, weights): @@ -303,7 +315,7 @@ def _transform_data(self, matrix, objectives, weights, **kwargs): class SKCDecisionMakerABC(SKCMethodABC): """Mixin class for all decisor based methods in scikit-criteria.""" - _skcriteria_abstract_method = True + _skcriteria_abstract_class = True _skcriteria_dm_type = "decision_maker" diff --git a/skcriteria/filters.ipynb b/skcriteria/filters.ipynb index 2c17d42..28af4eb 100644 --- a/skcriteria/filters.ipynb +++ b/skcriteria/filters.ipynb @@ -9,7 +9,7 @@ "import skcriteria as skc\n", "from skcriteria.core.methods import SKCTransformerABC\n", "import numpy as np\n", - "import abc\n" + "import abc" ] }, { @@ -281,13 +281,13 @@ " crit_array = matrix[:, crit_idx].flatten()\n", " crit_mask = np.apply_along_axis(flt_func, axis=0, arr=crit_array)\n", " mask_list.append(crit_mask)\n", - " \n", + "\n", " mask = np.all(np.column_stack(mask_list), axis=1)\n", "\n", " return mask\n", "\n", "\n", - "Filter(ROE=lambda e: e > 1, RI=lambda e: e >= 28).transform(dm)\n" + "Filter(ROE=lambda e: e > 1, RI=lambda e: e >= 28).transform(dm)" ] }, { diff --git a/skcriteria/madm/electre.py b/skcriteria/madm/electre.py index f8225ba..4edcc6b 100644 --- a/skcriteria/madm/electre.py +++ b/skcriteria/madm/electre.py @@ -143,6 +143,7 @@ class ELECTRE1(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = ["p", "q"] def __init__(self, p=0.65, q=0.35): diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index 761baa7..b248c44 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -129,6 +129,7 @@ class TOPSIS(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = ["metric"] def __init__(self, *, metric="euclidean"): diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index f3f20a6..a152937 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -56,7 +56,6 @@ class SKCPipeline(SKCMethodABC): _skcriteria_dm_type = "pipeline" _skcriteria_parameters = ["steps"] - def __init__(self, steps): self._validate_steps(steps) self.steps = list(steps) diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index 0019927..f306664 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -85,6 +85,7 @@ class EqualWeighter(SKCWeighterABC): total criteria. """ + _skcriteria_parameters = ["base_value"] def __init__(self, base_value=1.0): @@ -143,6 +144,7 @@ def std_weights(matrix): class StdWeighter(SKCWeighterABC): """Set as weight the normalized standard deviation of each criterion.""" + _skcriteria_parameters = [] @doc_inherit(SKCWeighterABC._weight_matrix) @@ -195,6 +197,7 @@ class EntropyWeighter(SKCWeighterABC): Calculate the entropy of a distribution for given probability values. """ + _skcriteria_parameters = [] @doc_inherit(SKCWeighterABC._weight_matrix) diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index bf75d2e..22ce65a 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -86,6 +86,9 @@ class Base(methods.SKCMethodABC): _skcriteria_dm_type = "foo" _skcriteria_parameters = ["x"] + def __init__(self, x): + pass + class Foo(Base): def __init__(self, x): pass @@ -93,12 +96,27 @@ def __init__(self, x): assert Foo._skcriteria_parameters == {"x"} +def test_SKCMethodABC_params_not_in_init(): + class Base(methods.SKCMethodABC): + _skcriteria_dm_type = "foo" + _skcriteria_parameters = ["x"] + + def __init__(self, x): + pass + + with pytest.raises(TypeError): + + class Foo(Base): + def __init__(self): + pass + + # ============================================================================= # TRANSFORMER # ============================================================================= -def test_not_redefined_SKCTransformerMixin(): +def test_SKCTransformerMixin_not_redefined_abc_methods(): class Foo(methods.SKCTransformerABC): _skcriteria_parameters = [] @@ -111,7 +129,7 @@ class Foo(methods.SKCTransformerABC): # ============================================================================= -def test_transform_data_not_implemented_SKCMatrixAndWeightTransformerMixin( +def test_SKCMatrixAndWeightTransformerMixin_transform_data_not_implemented( decision_matrix, ): class Foo(methods.SKCTransformerABC): @@ -127,7 +145,7 @@ def _transform_data(self, **kwargs): transformer.transform(dm) -def test_not_redefined_SKCMatrixAndWeightTransformerMixin(): +def test_SKCMatrixAndWeightTransformerMixin_not_redefined_abc_methods(): class Foo(methods.SKCMatrixAndWeightTransformerABC): pass @@ -141,7 +159,7 @@ class Foo(methods.SKCMatrixAndWeightTransformerABC): Foo("both") -def test_bad_normalize_for_SKCMatrixAndWeightTransformerMixin(): +def test_SKCMatrixAndWeightTransformerMixin_bad_normalize_for(): class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): ... @@ -153,7 +171,7 @@ def _transform_weights(self, weights): Foo("mtx") -def test_transform_weights_not_implemented_SKCMatrixAndWeightTransformerMixin( +def test_SKCMatrixAndWeightTransformerMixin_transform_weights_not_implemented( decision_matrix, ): class Foo(methods.SKCMatrixAndWeightTransformerABC): @@ -170,7 +188,7 @@ def _transform_weights(self, weights): transformer.transform(dm) -def test_transform_weight_not_implemented_SKCMatrixAndWeightTransformerMixin( +def test_SKCMatrixAndWeightTransformerMixin_transform_weight_not_implemented( decision_matrix, ): class Foo(methods.SKCMatrixAndWeightTransformerABC): @@ -213,7 +231,7 @@ def _transform_weights(self, weights): # ============================================================================= -def test_weight_matrix_not_implemented_SKCWeighterMixin(decision_matrix): +def test_SKCWeighterMixin_weight_matrix_not_implemented(decision_matrix): class Foo(methods.SKCWeighterABC): _skcriteria_parameters = [] @@ -227,7 +245,7 @@ def _weight_matrix(self, **kwargs): transformer.transform(dm) -def test_not_redefined_SKCWeighterMixin(): +def test_SKCWeighterMixin_not_redefined_abc_methods(): class Foo(methods.SKCWeighterABC): _skcriteria_parameters = [] @@ -235,7 +253,7 @@ class Foo(methods.SKCWeighterABC): Foo() -def test_flow_SKCWeighterMixin(decision_matrix): +def test_SKCWeighterMixin_flow(decision_matrix): dm = decision_matrix(seed=42) expected_weights = np.ones(dm.matrix.shape[1]) * 42 @@ -267,12 +285,12 @@ def _weight_matrix(self, matrix, **kwargs): # ============================================================================= -def test_flow_SKCDecisionMakerMixin(decision_matrix): +def test_SKCDecisionMakerMixin_flow(decision_matrix): dm = decision_matrix(seed=42) class Foo(methods.SKCDecisionMakerABC): - _skcriteria_parameters = ["x"] + _skcriteria_parameters = [] def _evaluate_data(self, alternatives, **kwargs): return np.arange(len(alternatives)) + 1, {} @@ -294,8 +312,8 @@ def _make_result(self, alternatives, values, extra): @pytest.mark.parametrize("not_redefine", ["_evaluate_data", "_make_result"]) -def test_not_redefined_SKCDecisionMakerMixin(not_redefine): - content = {"_skcriteria_parameters": ["x"]} +def test_SKCDecisionMakerMixin_not_redefined(not_redefine): + content = {"_skcriteria_parameters": []} for method_name in ["_evaluate_data", "_make_result", "_validate_data"]: if method_name != not_redefine: content[method_name] = lambda **kws: None @@ -306,7 +324,7 @@ def test_not_redefined_SKCDecisionMakerMixin(not_redefine): Foo() -def test_evaluate_data_not_implemented_SKCDecisionMakerMixin(decision_matrix): +def test_SKCDecisionMakerMixin_evaluate_data_not_implemented(decision_matrix): dm = decision_matrix(seed=42) @@ -329,7 +347,7 @@ def _make_result(self, alternatives, values, extra): ranker.evaluate(dm) -def test_make_result_not_implemented_SKCDecisionMakerMixin(decision_matrix): +def test_SKCDecisionMakerMixin_make_result_not_implemented(decision_matrix): dm = decision_matrix(seed=42) @@ -353,14 +371,14 @@ def _make_result(self, **kwargs): def _get_subclasses(cls): - dmtype = getattr(cls, "_skcriteria_dm_type", None) + is_abstract = vars(cls).get("_skcriteria_abstract_class", False) - if dmtype not in (None, "pipeline") and not cls.__abstractmethods__: + if not is_abstract: yield cls - for subc in cls.__subclasses__(): - for subsub in _get_subclasses(subc): - yield subsub + for subc in cls.__subclasses__(): + for subsub in _get_subclasses(subc): + yield subsub @pytest.mark.run(order=-1) From 44a0ade3004cffd48e7e782f44517947e6096c1c Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 18:15:48 -0300 Subject: [PATCH 21/47] metal level brutal refactor --- skcriteria/core/__init__.py | 10 - skcriteria/core/data.py | 220 +--------- skcriteria/core/methods.py | 104 +---- skcriteria/filters.ipynb | 413 ------------------ skcriteria/madm/__init__.py | 8 + skcriteria/madm/_base.py | 287 ++++++++++++ skcriteria/madm/electre.py | 5 +- skcriteria/madm/moora.py | 8 +- skcriteria/madm/similarity.py | 4 +- skcriteria/madm/simple.py | 5 +- skcriteria/madm/simus.py | 5 +- skcriteria/preprocessing/distance.py | 1 + skcriteria/preprocessing/increment.py | 1 + skcriteria/preprocessing/invert_objectives.py | 1 + skcriteria/preprocessing/weighters.py | 48 +- tests/core/test_data.py | 210 --------- tests/core/test_methods.py | 148 +------ tests/madm/test_base.py | 325 ++++++++++++++ tests/madm/test_moora.py | 2 +- tests/madm/test_similarity.py | 2 +- tests/madm/test_simple.py | 2 +- tests/madm/test_simus.py | 2 +- tests/preprocessing/test_weighters.py | 56 +++ 23 files changed, 764 insertions(+), 1103 deletions(-) delete mode 100644 skcriteria/filters.ipynb create mode 100644 skcriteria/madm/_base.py create mode 100644 tests/madm/test_base.py diff --git a/skcriteria/core/__init__.py b/skcriteria/core/__init__.py index 79ac179..0716b0c 100644 --- a/skcriteria/core/__init__.py +++ b/skcriteria/core/__init__.py @@ -17,18 +17,13 @@ from .data import ( DecisionMatrix, - KernelResult, Objective, - RankResult, - ResultABC, mkdm, ) from .methods import ( - SKCDecisionMakerABC, SKCMatrixAndWeightTransformerABC, SKCMethodABC, SKCTransformerABC, - SKCWeighterABC, ) from .plot import DecisionMatrixPlotter @@ -40,13 +35,8 @@ "mkdm", "DecisionMatrix", "DecisionMatrixPlotter", - "KernelResult", "Objective", - "RankResult", - "ResultABC", - "SKCDecisionMakerABC", "SKCMatrixAndWeightTransformerABC", "SKCMethodABC", "SKCTransformerABC", - "SKCWeighterABC", ] diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index faf4bf2..fb14b68 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -20,7 +20,7 @@ # IMPORTS # ============================================================================= -import abc + import enum import functools @@ -33,7 +33,7 @@ from .plot import DecisionMatrixPlotter -from ..utils import Bunch, deprecated, doc_inherit +from ..utils import deprecated # ============================================================================= @@ -794,219 +794,3 @@ def _repr_html_(self): def mkdm(*args, **kwargs): """Alias for DecisionMatrix.from_mcda_data.""" return DecisionMatrix.from_mcda_data(*args, **kwargs) - - -# ============================================================================= -# RESULTS -# ============================================================================= - - -class ResultABC(metaclass=abc.ABCMeta): - """Base class to implement different types of results. - - Any evaluation of the DecisionMatrix is expected to result in an object - that extends the functionalities of this class. - - Parameters - ---------- - method: str - Name of the method that generated the result. - alternatives: array-like - Names of the alternatives evaluated. - values: array-like - Values assigned to each alternative by the method, where the i-th - value refers to the valuation of the i-th. alternative. - extra: dict-like - Extra information provided by the method regarding the evaluation of - the alternatives. - - """ - - _skcriteria_result_column = None - - def __init_subclass__(cls): - """Validate if the subclass are well formed.""" - result_column = cls._skcriteria_result_column - if result_column is None: - raise TypeError(f"{cls} must redefine '_skcriteria_result_column'") - - def __init__(self, method, alternatives, values, extra): - self._validate_result(values) - self._method = str(method) - self._extra = Bunch("extra", extra) - self._result_df = pd.DataFrame( - values, - index=alternatives, - columns=[self._skcriteria_result_column], - ) - - @abc.abstractmethod - def _validate_result(self, values): - """Validate that the values are the expected by the result type.""" - raise NotImplementedError() - - @property - def values(self): - """Values assigned to each alternative by the method. - - The i-th value refers to the valuation of the i-th. alternative. - - """ - return self._result_df[self._skcriteria_result_column].to_numpy() - - @property - def method(self): - """Name of the method that generated the result.""" - return self._method - - @property - def alternatives(self): - """Names of the alternatives evaluated.""" - return self._result_df.index.to_numpy() - - @property - def extra_(self): - """Additional information about the result. - - Note - ---- - ``e_`` is an alias for this property - - """ - return self._extra - - e_ = extra_ - - # CMP ===================================================================== - - @property - def shape(self): - """Tuple with (number_of_alternatives, number_of_alternatives). - - rank.shape <==> np.shape(rank) - - """ - return np.shape(self._result_df) - - def __len__(self): - """Return the number ot alternatives. - - rank.__len__() <==> len(rank). - - """ - return len(self._result_df) - - def equals(self, other): - """Check if the alternatives and ranking are the same. - - The method doesn't check the method or the extra parameters. - - """ - return (self is other) or ( - isinstance(other, RankResult) - and self._result_df.equals(other._result_df) - ) - - # REPR ==================================================================== - - def __repr__(self): - """result.__repr__() <==> repr(result).""" - kwargs = {"show_dimensions": False} - - # retrieve the original string - df = self._result_df.T - original_string = df.to_string(**kwargs) - - # add dimension - string = f"{original_string}\n[Method: {self.method}]" - - return string - - -@doc_inherit(ResultABC) -class RankResult(ResultABC): - """Ranking of alternatives. - - This type of results is used by methods that generate a ranking of - alternatives. - - """ - - _skcriteria_result_column = "Rank" - - @doc_inherit(ResultABC._validate_result) - def _validate_result(self, values): - length = len(values) - expected = np.arange(length) + 1 - if not np.array_equal(np.sort(values), expected): - raise ValueError(f"The data {values} doesn't look like a ranking") - - @property - def rank_(self): - """Alias for ``values``.""" - return self.values - - def _repr_html_(self): - """Return a html representation for a particular result. - - Mainly for IPython notebook. - - """ - df = self._result_df.T - original_html = df.style._repr_html_() - - # add metadata - html = ( - "
\n" - f"{original_html}" - f"Method: {self.method}\n" - "
" - ) - - return html - - -@doc_inherit(ResultABC) -class KernelResult(ResultABC): - """Separates the alternatives between good (kernel) and bad. - - This type of results is used by methods that select which alternatives - are good and bad. The good alternatives are called "kernel" - - """ - - _skcriteria_result_column = "Kernel" - - @doc_inherit(ResultABC._validate_result) - def _validate_result(self, values): - if np.asarray(values).dtype != bool: - raise ValueError(f"The data {values} doesn't look like a kernel") - - @property - def kernel_(self): - """Alias for ``values``.""" - return self.values - - @property - def kernelwhere_(self): - """Indexes of the alternatives that are part of the kernel.""" - return np.where(self.kernel_)[0] - - def _repr_html_(self): - """Return a html representation for a particular result. - - Mainly for IPython notebook. - - """ - df = self._result_df.T - original_html = df._repr_html_() - - # add metadata - html = ( - "
\n" - f"{original_html}" - f"Method: {self.method}\n" - "
" - ) - - return html diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index de11acc..06ed3c1 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -62,8 +62,14 @@ def __init_subclass__(cls): params = frozenset(params) signature = inspect.signature(cls.__init__) + has_kwargs = any( + p.kind == inspect.Parameter.VAR_KEYWORD + for p in signature.parameters.values() + ) + params_not_in_signature = params.difference(signature.parameters) - if params_not_in_signature: + if params_not_in_signature and not has_kwargs: + raise TypeError( f"{cls} defines the parameters {params_not_in_signature} " "which is not found as a parameter in the __init__ method." @@ -255,99 +261,3 @@ def _transform_data(self, matrix, weights, **kwargs): ) return kwargs - - -# ============================================================================= -# SK WEIGHTER -# ============================================================================= - - -class SKCWeighterABC(SKCTransformerABC): - """Mixin capable of determine the weights of the matrix. - - This mixin require to redefine ``_weight_matrix``, instead of - ``_transform_data``. - - """ - - _skcriteria_abstract_class = True - - @abc.abstractmethod - def _weight_matrix(self, matrix, objectives, weights): - """Calculate a new array of weights. - - Parameters - ---------- - matrix: :py:class:`numpy.ndarray` - The decision matrix to weights. - objectives: :py:class:`numpy.ndarray` - The objectives in numeric format. - weights: :py:class:`numpy.ndarray` - The original weights - - Returns - ------- - :py:class:`numpy.ndarray` - An array of weights. - - """ - raise NotImplementedError() - - @doc_inherit(SKCTransformerABC._transform_data) - def _transform_data(self, matrix, objectives, weights, **kwargs): - - new_weights = self._weight_matrix( - matrix=matrix, objectives=objectives, weights=weights - ) - - kwargs.update( - matrix=matrix, objectives=objectives, weights=new_weights - ) - - return kwargs - - -# ============================================================================= -# -# ============================================================================= - - -class SKCDecisionMakerABC(SKCMethodABC): - """Mixin class for all decisor based methods in scikit-criteria.""" - - _skcriteria_abstract_class = True - - _skcriteria_dm_type = "decision_maker" - - @abc.abstractmethod - def _evaluate_data(self, **kwargs): - raise NotImplementedError() - - @abc.abstractmethod - def _make_result(self, alternatives, values, extra): - raise NotImplementedError() - - def evaluate(self, dm): - """Validate the dm and calculate and evaluate the alternatives. - - Parameters - ---------- - dm: :py:class:`skcriteria.data.DecisionMatrix` - Decision matrix on which the ranking will be calculated. - - Returns - ------- - :py:class:`skcriteria.data.RankResult` - Ranking. - - """ - data = dm.to_dict() - - result_data, extra = self._evaluate_data(**data) - - alternatives = data["alternatives"] - result = self._make_result( - alternatives=alternatives, values=result_data, extra=extra - ) - - return result diff --git a/skcriteria/filters.ipynb b/skcriteria/filters.ipynb deleted file mode 100644 index 28af4eb..0000000 --- a/skcriteria/filters.ipynb +++ /dev/null @@ -1,413 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import skcriteria as skc\n", - "from skcriteria.core.methods import SKCTransformerABC\n", - "import numpy as np\n", - "import abc" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "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", - " ROE[▲ 2.0]CAP[▲ 4.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
MM1730
FN5830
\n", - "
5 Alternatives x 3 Criteria\n", - "
" - ], - "text/plain": [ - " ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0]\n", - "PE 7 5 35\n", - "JN 5 4 26\n", - "AA 5 6 28\n", - "MM 1 7 30\n", - "FN 5 8 30\n", - "[5 Alternatives x 3 Criteria]" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dm = skc.mkdm(\n", - " matrix=[[7, 5, 35], [5, 4, 26], [5, 6, 28], [1, 7, 30], [5, 8, 30]],\n", - " objectives=[max, max, min],\n", - " weights=[2, 4, 1],\n", - " alternatives=[\"PE\", \"JN\", \"AA\", \"MM\", \"FN\"],\n", - " criteria=[\"ROE\", \"CAP\", \"RI\"],\n", - ")\n", - "\n", - "dm" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "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", - " C0[▲ 2.0]C1[▲ 4.0]C2[▼ 1.0]
PE7535
AA5628
FN5830
\n", - "
3 Alternatives x 3 Criteria\n", - "
" - ], - "text/plain": [ - " C0[▲ 2.0] C1[▲ 4.0] C2[▼ 1.0]\n", - "PE 7 5 35\n", - "AA 5 6 28\n", - "FN 5 8 30\n", - "[3 Alternatives x 3 Criteria]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "class SKCFilterABC(SKCTransformerABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - "\n", - " def __init__(self, **criteria_filters):\n", - " if not criteria_filters:\n", - " raise ValueError()\n", - " self._criteria_filters = criteria_filters\n", - "\n", - " @property\n", - " def criteria_filters(self):\n", - " return dict(self._criteria_filters)\n", - "\n", - " @abc.abstractmethod\n", - " def _make_mask(self, matrix, criteria):\n", - " raise NotImplementedError()\n", - "\n", - " def _transform_data(self, matrix, criteria, alternatives, **kwargs):\n", - " criteria_not_found = set(self._criteria_filters).difference(criteria)\n", - " if criteria_not_found:\n", - " raise ValueError(f\"Missing criteria: {criteria_not_found}\")\n", - "\n", - " mask = self._make_mask(matrix, criteria)\n", - "\n", - " filtered_matrix = matrix[mask]\n", - " filtered_alternatives = alternatives[mask]\n", - "\n", - " kwargs.update(\n", - " matrix=filtered_matrix,\n", - " alternatives=filtered_alternatives,\n", - " dtypes=None,\n", - " )\n", - " return kwargs\n", - "\n", - "\n", - "class SKCFilterOperatorABC(SKCFilterABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - "\n", - " @property\n", - " @abc.abstractmethod\n", - " def _filter(self, arr, cond):\n", - " raise NotImplementedError()\n", - "\n", - " def _make_mask(self, matrix, criteria):\n", - " cnames, climits = [], []\n", - " for cname, climit in self._criteria_filters.items():\n", - " cnames.append(cname)\n", - " climits.append(climit)\n", - "\n", - " idxs = np.in1d(criteria, cnames)\n", - " matrix = matrix[:, idxs]\n", - " mask = np.all(self._filter(matrix, climits), axis=1)\n", - "\n", - " return mask\n", - "\n", - "\n", - "class FilterGT(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.greater\n", - "\n", - "\n", - "class FilterGE(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.greater_equal\n", - "\n", - "\n", - "class FilterLT(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.less\n", - "\n", - "\n", - "class FilterLE(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.less_equal\n", - "\n", - "\n", - "class FilterEQ(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.equal\n", - "\n", - "\n", - "class FilterNE(SKCFilterOperatorABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - " _filter = np.not_equal\n", - "\n", - "\n", - "class Filter(SKCFilterABC):\n", - "\n", - " _skcriteria_parameters = frozenset([\"criteria_filters\"])\n", - "\n", - " def _make_mask(self, matrix, criteria):\n", - " mask_list = []\n", - " for cname, flt_func in self._criteria_filters.items():\n", - " crit_idx = np.in1d(criteria, cname, assume_unique=False)\n", - " crit_array = matrix[:, crit_idx].flatten()\n", - " crit_mask = np.apply_along_axis(flt_func, axis=0, arr=crit_array)\n", - " mask_list.append(crit_mask)\n", - "\n", - " mask = np.all(np.column_stack(mask_list), axis=1)\n", - "\n", - " return mask\n", - "\n", - "\n", - "Filter(ROE=lambda e: e > 1, RI=lambda e: e >= 28).transform(dm)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "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", - " ROE[▲ 2.0]CAP[▲ 4.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
MM1730
FN5830
\n", - "
5 Alternatives x 3 Criteria\n", - "
" - ], - "text/plain": [ - " ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0]\n", - "PE 7 5 35\n", - "JN 5 4 26\n", - "AA 5 6 28\n", - "MM 1 7 30\n", - "FN 5 8 30\n", - "[5 Alternatives x 3 Criteria]" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dm" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "interpreter": { - "hash": "0a948a28a0ef94db8982fe8442467cdefb8d68ae66a4b8f3824263fcc702cc89" - }, - "kernelspec": { - "display_name": "Python 3.9.10 64-bit ('skcriteria': virtualenv)", - "language": "python", - "name": "python3" - }, - "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.10" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/skcriteria/madm/__init__.py b/skcriteria/madm/__init__.py index 340d44f..13b30fe 100644 --- a/skcriteria/madm/__init__.py +++ b/skcriteria/madm/__init__.py @@ -10,3 +10,11 @@ # ============================================================================= """MCDA methods.""" + +from ._base import KernelResult, RankResult, ResultABC, SKCDecisionMakerABC + +# ============================================================================= +# ALL +# ============================================================================= + +__all__ = ["KernelResult", "RankResult", "ResultABC", "SKCDecisionMakerABC"] diff --git a/skcriteria/madm/_base.py b/skcriteria/madm/_base.py new file mode 100644 index 0000000..c786f47 --- /dev/null +++ b/skcriteria/madm/_base.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Core functionalities to create madm decision-maker classes.""" + + +# ============================================================================= +# imports +# ============================================================================= + +import abc + +import numpy as np + +import pandas as pd + +from ..core import SKCMethodABC +from ..utils import Bunch, doc_inherit + +# ============================================================================= +# DM BASE +# ============================================================================= + + +class SKCDecisionMakerABC(SKCMethodABC): + """Mixin class for all decisor based methods in scikit-criteria.""" + + _skcriteria_abstract_class = True + + _skcriteria_dm_type = "decision_maker" + + @abc.abstractmethod + def _evaluate_data(self, **kwargs): + raise NotImplementedError() + + @abc.abstractmethod + def _make_result(self, alternatives, values, extra): + raise NotImplementedError() + + def evaluate(self, dm): + """Validate the dm and calculate and evaluate the alternatives. + + Parameters + ---------- + dm: :py:class:`skcriteria.data.DecisionMatrix` + Decision matrix on which the ranking will be calculated. + + Returns + ------- + :py:class:`skcriteria.data.RankResult` + Ranking. + + """ + data = dm.to_dict() + + result_data, extra = self._evaluate_data(**data) + + alternatives = data["alternatives"] + result = self._make_result( + alternatives=alternatives, values=result_data, extra=extra + ) + + return result + + +# ============================================================================= +# RESULTS +# ============================================================================= + + +class ResultABC(metaclass=abc.ABCMeta): + """Base class to implement different types of results. + + Any evaluation of the DecisionMatrix is expected to result in an object + that extends the functionalities of this class. + + Parameters + ---------- + method: str + Name of the method that generated the result. + alternatives: array-like + Names of the alternatives evaluated. + values: array-like + Values assigned to each alternative by the method, where the i-th + value refers to the valuation of the i-th. alternative. + extra: dict-like + Extra information provided by the method regarding the evaluation of + the alternatives. + + """ + + _skcriteria_result_column = None + + def __init_subclass__(cls): + """Validate if the subclass are well formed.""" + result_column = cls._skcriteria_result_column + if result_column is None: + raise TypeError(f"{cls} must redefine '_skcriteria_result_column'") + + def __init__(self, method, alternatives, values, extra): + self._validate_result(values) + self._method = str(method) + self._extra = Bunch("extra", extra) + self._result_df = pd.DataFrame( + values, + index=alternatives, + columns=[self._skcriteria_result_column], + ) + + @abc.abstractmethod + def _validate_result(self, values): + """Validate that the values are the expected by the result type.""" + raise NotImplementedError() + + @property + def values(self): + """Values assigned to each alternative by the method. + + The i-th value refers to the valuation of the i-th. alternative. + + """ + return self._result_df[self._skcriteria_result_column].to_numpy() + + @property + def method(self): + """Name of the method that generated the result.""" + return self._method + + @property + def alternatives(self): + """Names of the alternatives evaluated.""" + return self._result_df.index.to_numpy() + + @property + def extra_(self): + """Additional information about the result. + + Note + ---- + ``e_`` is an alias for this property + + """ + return self._extra + + e_ = extra_ + + # CMP ===================================================================== + + @property + def shape(self): + """Tuple with (number_of_alternatives, number_of_alternatives). + + rank.shape <==> np.shape(rank) + + """ + return np.shape(self._result_df) + + def __len__(self): + """Return the number ot alternatives. + + rank.__len__() <==> len(rank). + + """ + return len(self._result_df) + + def equals(self, other): + """Check if the alternatives and ranking are the same. + + The method doesn't check the method or the extra parameters. + + """ + return (self is other) or ( + isinstance(other, RankResult) + and self._result_df.equals(other._result_df) + ) + + # REPR ==================================================================== + + def __repr__(self): + """result.__repr__() <==> repr(result).""" + kwargs = {"show_dimensions": False} + + # retrieve the original string + df = self._result_df.T + original_string = df.to_string(**kwargs) + + # add dimension + string = f"{original_string}\n[Method: {self.method}]" + + return string + + +@doc_inherit(ResultABC) +class RankResult(ResultABC): + """Ranking of alternatives. + + This type of results is used by methods that generate a ranking of + alternatives. + + """ + + _skcriteria_result_column = "Rank" + + @doc_inherit(ResultABC._validate_result) + def _validate_result(self, values): + length = len(values) + expected = np.arange(length) + 1 + if not np.array_equal(np.sort(values), expected): + raise ValueError(f"The data {values} doesn't look like a ranking") + + @property + def rank_(self): + """Alias for ``values``.""" + return self.values + + def _repr_html_(self): + """Return a html representation for a particular result. + + Mainly for IPython notebook. + + """ + df = self._result_df.T + original_html = df.style._repr_html_() + + # add metadata + html = ( + "
\n" + f"{original_html}" + f"Method: {self.method}\n" + "
" + ) + + return html + + +@doc_inherit(ResultABC) +class KernelResult(ResultABC): + """Separates the alternatives between good (kernel) and bad. + + This type of results is used by methods that select which alternatives + are good and bad. The good alternatives are called "kernel" + + """ + + _skcriteria_result_column = "Kernel" + + @doc_inherit(ResultABC._validate_result) + def _validate_result(self, values): + if np.asarray(values).dtype != bool: + raise ValueError(f"The data {values} doesn't look like a kernel") + + @property + def kernel_(self): + """Alias for ``values``.""" + return self.values + + @property + def kernelwhere_(self): + """Indexes of the alternatives that are part of the kernel.""" + return np.where(self.kernel_)[0] + + def _repr_html_(self): + """Return a html representation for a particular result. + + Mainly for IPython notebook. + + """ + df = self._result_df.T + original_html = df._repr_html_() + + # add metadata + html = ( + "
\n" + f"{original_html}" + f"Method: {self.method}\n" + "
" + ) + + return html diff --git a/skcriteria/madm/electre.py b/skcriteria/madm/electre.py index 4edcc6b..fcef009 100644 --- a/skcriteria/madm/electre.py +++ b/skcriteria/madm/electre.py @@ -29,9 +29,12 @@ import numpy as np -from ..core import KernelResult, Objective, SKCDecisionMakerABC + +from ._base import KernelResult, SKCDecisionMakerABC +from ..core import Objective from ..utils import doc_inherit + # ============================================================================= # CONCORDANCE # ============================================================================= diff --git a/skcriteria/madm/moora.py b/skcriteria/madm/moora.py index 98dbc05..4280ab2 100644 --- a/skcriteria/madm/moora.py +++ b/skcriteria/madm/moora.py @@ -20,9 +20,11 @@ import numpy as np -from ..core import Objective, RankResult, SKCDecisionMakerABC +from ._base import RankResult, SKCDecisionMakerABC +from ..core import Objective from ..utils import doc_inherit, rank + # ============================================================================= # Ratio MOORA # ============================================================================= @@ -68,6 +70,7 @@ class RatioMOORA(SKCDecisionMakerABC): :cite:p:`brauers2006moora` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) @@ -132,6 +135,7 @@ class ReferencePointMOORA(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) @@ -219,6 +223,7 @@ class FullMultiplicativeForm(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) @@ -306,6 +311,7 @@ class MultiMOORA(SKCDecisionMakerABC): :cite:p:`brauers2012robustness` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) diff --git a/skcriteria/madm/similarity.py b/skcriteria/madm/similarity.py index b248c44..b3e60b1 100644 --- a/skcriteria/madm/similarity.py +++ b/skcriteria/madm/similarity.py @@ -21,9 +21,11 @@ from scipy.spatial import distance -from ..core import Objective, RankResult, SKCDecisionMakerABC +from ._base import RankResult, SKCDecisionMakerABC +from ..core import Objective from ..utils import doc_inherit, rank + # ============================================================================= # CONSTANTS # ============================================================================= diff --git a/skcriteria/madm/simple.py b/skcriteria/madm/simple.py index c28ce23..1f1a90a 100644 --- a/skcriteria/madm/simple.py +++ b/skcriteria/madm/simple.py @@ -18,9 +18,11 @@ import numpy as np -from ..core import Objective, RankResult, SKCDecisionMakerABC +from ._base import RankResult, SKCDecisionMakerABC +from ..core import Objective from ..utils import doc_inherit, rank + # ============================================================================= # SAM # ============================================================================= @@ -74,6 +76,7 @@ class WeightedSumModel(SKCDecisionMakerABC): :cite:p:`tzeng2011multiple` """ + _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) diff --git a/skcriteria/madm/simus.py b/skcriteria/madm/simus.py index 0ce91a2..fb3115b 100644 --- a/skcriteria/madm/simus.py +++ b/skcriteria/madm/simus.py @@ -20,10 +20,12 @@ import numpy as np -from ..core import Objective, RankResult, SKCDecisionMakerABC +from ._base import RankResult, SKCDecisionMakerABC +from ..core import Objective from ..preprocessing.scalers import scale_by_sum from ..utils import doc_inherit, lp, rank + # ============================================================================= # INTERNAL FUNCTIONS # ============================================================================= @@ -246,6 +248,7 @@ class SIMUS(SKCDecisionMakerABC): `PuLP Documentation `_ """ + _skcriteria_parameters = ["rank_by", "solver"] def __init__(self, *, rank_by=1, solver="pulp"): diff --git a/skcriteria/preprocessing/distance.py b/skcriteria/preprocessing/distance.py index e40047c..8ad7aca 100644 --- a/skcriteria/preprocessing/distance.py +++ b/skcriteria/preprocessing/distance.py @@ -82,6 +82,7 @@ class CenitDistance(SKCTransformerABC): :cite:p:`diakoulaki1995determining` """ + _skcriteria_parameters = [] @doc_inherit(SKCTransformerABC._transform_data) diff --git a/skcriteria/preprocessing/increment.py b/skcriteria/preprocessing/increment.py index f91997f..d3410a8 100644 --- a/skcriteria/preprocessing/increment.py +++ b/skcriteria/preprocessing/increment.py @@ -86,6 +86,7 @@ class AddValueToZero(SKCMatrixAndWeightTransformerABC): \overline{X}_{ij} = X_{ij} + value """ + _skcriteria_parameters = ["target", "value"] def __init__(self, target, value=1.0): diff --git a/skcriteria/preprocessing/invert_objectives.py b/skcriteria/preprocessing/invert_objectives.py index 3ed75b1..3d6f67d 100644 --- a/skcriteria/preprocessing/invert_objectives.py +++ b/skcriteria/preprocessing/invert_objectives.py @@ -93,6 +93,7 @@ class MinimizeToMaximize(SKCTransformerABC): ones thar are converted to ``numpy.float64``. """ + _skcriteria_parameters = [] @doc_inherit(SKCTransformerABC._transform_data) diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index f306664..0c4a7eb 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -21,6 +21,7 @@ # IMPORTS # ============================================================================= +import abc import warnings import numpy as np @@ -29,10 +30,55 @@ from .distance import cenit_distance -from ..core import Objective, SKCWeighterABC +from ..core import Objective, SKCTransformerABC from ..utils import doc_inherit +class SKCWeighterABC(SKCTransformerABC): + """Mixin capable of determine the weights of the matrix. + + This mixin require to redefine ``_weight_matrix``, instead of + ``_transform_data``. + + """ + + _skcriteria_abstract_class = True + + @abc.abstractmethod + def _weight_matrix(self, matrix, objectives, weights): + """Calculate a new array of weights. + + Parameters + ---------- + matrix: :py:class:`numpy.ndarray` + The decision matrix to weights. + objectives: :py:class:`numpy.ndarray` + The objectives in numeric format. + weights: :py:class:`numpy.ndarray` + The original weights + + Returns + ------- + :py:class:`numpy.ndarray` + An array of weights. + + """ + raise NotImplementedError() + + @doc_inherit(SKCTransformerABC._transform_data) + def _transform_data(self, matrix, objectives, weights, **kwargs): + + new_weights = self._weight_matrix( + matrix=matrix, objectives=objectives, weights=weights + ) + + kwargs.update( + matrix=matrix, objectives=objectives, weights=new_weights + ) + + return kwargs + + # ============================================================================= # SAME WEIGHT # ============================================================================= diff --git a/tests/core/test_data.py b/tests/core/test_data.py index 47f9cd8..ba435e8 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -777,213 +777,3 @@ def test_DecisionMatrix_mtx_ndim3(data_values): alternatives=alternatives, criteria=criteria, ) - - -# ============================================================================= -# RESULT BASE -# ============================================================================= - - -class test_ResultBase_skacriteria_result_column_no_defined: - - with pytest.raises(TypeError): - - class Foo(data.ResultABC): - def _validate_result(self, values): - pass - - -class test_ResultBase_original_validare_result_fail: - class Foo(data.ResultABC): - _skcriteria_result_column = "foo" - - def _validate_result(self, values): - return super()._validate_result(values) - - with pytest.raises(NotImplementedError): - Foo("foo", ["abc"], [1, 2, 3], {}) - - -# ============================================================================= -# RANK RESULT -# ============================================================================= - - -def test_RankResult(): - method = "foo" - alternatives = ["a", "b", "c"] - rank = [1, 2, 3] - extra = {"alfa": 1} - - result = data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - ) - - assert np.all(result.method == method) - assert np.all(result.alternatives == alternatives) - assert np.all(result.rank_ == rank) - assert np.all(result.extra_ == result.e_ == extra) - - -@pytest.mark.parametrize("rank", [[1, 2, 5], [1, 1, 1], [1, 2, 2], [1, 2]]) -def test_RankResult_invalid_rank(rank): - method = "foo" - alternatives = ["a", "b", "c"] - extra = {"alfa": 1} - - with pytest.raises(ValueError): - data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - ) - - -def test_RankResult_shape(): - random = np.random.default_rng(seed=42) - length = random.integers(10, 100) - - rank = np.arange(length) + 1 - alternatives = [f"A.{r}" for r in rank] - method = "foo" - extra = {} - - result = data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - ) - - assert result.shape == (length, 1) - - -def test_RankResult_len(): - random = np.random.default_rng(seed=42) - length = random.integers(10, 100) - - rank = np.arange(length) + 1 - alternatives = [f"A.{r}" for r in rank] - method = "foo" - extra = {} - - result = data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - ) - - assert len(result) == length - - -def test_RankResult_repr(): - method = "foo" - alternatives = ["a", "b", "c"] - rank = [1, 2, 3] - extra = {"alfa": 1} - - result = data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - ) - - expected = " a b c\n" "Rank 1 2 3\n" "[Method: foo]" - - assert repr(result) == expected - - -def test_RankResult_repr_html(): - method = "foo" - alternatives = ["a", "b", "c"] - rank = [1, 2, 3] - extra = {"alfa": 1} - - result = PyQuery( - data.RankResult( - method=method, alternatives=alternatives, values=rank, extra=extra - )._repr_html_() - ) - - expected = PyQuery( - """ -
- - - - - - - - - - - - - - - - - -
abc
- Rank - 123
- Method: foo -
- """ - ) - assert result.remove("style").text() == expected.remove("style").text() - - -# ============================================================================= -# KERNEL -# ============================================================================= - - -@pytest.mark.parametrize("values", [[1, 2, 5], [True, False, 1], [1, 2, 3]]) -def test_KernelResult_invalid_rank(values): - method = "foo" - alternatives = ["a", "b", "c"] - extra = {"alfa": 1} - - with pytest.raises(ValueError): - data.KernelResult( - method=method, - alternatives=alternatives, - values=values, - extra=extra, - ) - - -def test_KernelResult_repr_html(): - method = "foo" - alternatives = ["a", "b", "c"] - rank = [True, False, True] - extra = {"alfa": 1} - - result = PyQuery( - data.KernelResult( - method=method, alternatives=alternatives, values=rank, extra=extra - )._repr_html_() - ) - - expected = PyQuery( - """ -
- - - - - - - - - - - - - - - - - -
abc
- Kernel - TrueFalseTrue
- Method: foo -
- """ - ) - - assert result.remove("style").text() == expected.remove("style").text() diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 22ce65a..36d76ac 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -18,11 +18,9 @@ # IMPORTS # ============================================================================= -import numpy as np - import pytest -from skcriteria.core import data, methods +from skcriteria.core import methods # ============================================================================= @@ -96,12 +94,12 @@ def __init__(self, x): assert Foo._skcriteria_parameters == {"x"} -def test_SKCMethodABC_params_not_in_init(): +def test_SKCMethodABC_params_in_init(): class Base(methods.SKCMethodABC): _skcriteria_dm_type = "foo" _skcriteria_parameters = ["x"] - def __init__(self, x): + def __init__(self, **kwargs): pass with pytest.raises(TypeError): @@ -226,146 +224,6 @@ def _transform_weights(self, weights): assert foo.target == Foo._TARGET_BOTH -# ============================================================================= -# MATRIX AND WEIGHT TRANSFORMER -# ============================================================================= - - -def test_SKCWeighterMixin_weight_matrix_not_implemented(decision_matrix): - class Foo(methods.SKCWeighterABC): - _skcriteria_parameters = [] - - def _weight_matrix(self, **kwargs): - return super()._weight_matrix(**kwargs) - - transformer = Foo() - dm = decision_matrix(seed=42) - - with pytest.raises(NotImplementedError): - transformer.transform(dm) - - -def test_SKCWeighterMixin_not_redefined_abc_methods(): - class Foo(methods.SKCWeighterABC): - _skcriteria_parameters = [] - - with pytest.raises(TypeError): - Foo() - - -def test_SKCWeighterMixin_flow(decision_matrix): - - dm = decision_matrix(seed=42) - expected_weights = np.ones(dm.matrix.shape[1]) * 42 - - class Foo(methods.SKCWeighterABC): - _skcriteria_parameters = [] - - def _weight_matrix(self, matrix, **kwargs): - return expected_weights - - transformer = Foo() - - expected = data.mkdm( - matrix=dm.matrix, - objectives=dm.objectives, - weights=expected_weights, - dtypes=dm.dtypes, - alternatives=dm.alternatives, - criteria=dm.criteria, - ) - - result = transformer.transform(dm) - - assert result.equals(expected) - - -# ============================================================================= -# SKCDecisionMakerABC -# ============================================================================= - - -def test_SKCDecisionMakerMixin_flow(decision_matrix): - - dm = decision_matrix(seed=42) - - class Foo(methods.SKCDecisionMakerABC): - _skcriteria_parameters = [] - - def _evaluate_data(self, alternatives, **kwargs): - return np.arange(len(alternatives)) + 1, {} - - def _make_result(self, alternatives, values, extra): - return { - "alternatives": alternatives, - "rank": values, - "extra": extra, - } - - ranker = Foo() - - result = ranker.evaluate(dm) - - assert np.all(result["alternatives"] == dm.alternatives) - assert np.all(result["rank"] == np.arange(len(dm.alternatives)) + 1) - assert result["extra"] == {} - - -@pytest.mark.parametrize("not_redefine", ["_evaluate_data", "_make_result"]) -def test_SKCDecisionMakerMixin_not_redefined(not_redefine): - content = {"_skcriteria_parameters": []} - for method_name in ["_evaluate_data", "_make_result", "_validate_data"]: - if method_name != not_redefine: - content[method_name] = lambda **kws: None - - Foo = type("Foo", (methods.SKCDecisionMakerABC,), content) - - with pytest.raises(TypeError): - Foo() - - -def test_SKCDecisionMakerMixin_evaluate_data_not_implemented(decision_matrix): - - dm = decision_matrix(seed=42) - - class Foo(methods.SKCDecisionMakerABC): - _skcriteria_parameters = [] - - def _evaluate_data(self, **kwargs): - super()._evaluate_data(**kwargs) - - def _make_result(self, alternatives, values, extra): - return { - "alternatives": alternatives, - "rank": values, - "extra": extra, - } - - ranker = Foo() - - with pytest.raises(NotImplementedError): - ranker.evaluate(dm) - - -def test_SKCDecisionMakerMixin_make_result_not_implemented(decision_matrix): - - dm = decision_matrix(seed=42) - - class Foo(methods.SKCDecisionMakerABC): - _skcriteria_parameters = [] - - def _evaluate_data(self, alternatives, **kwargs): - return np.arange(len(alternatives)) + 1, {} - - def _make_result(self, **kwargs): - super()._make_result(**kwargs) - - ranker = Foo() - - with pytest.raises(NotImplementedError): - ranker.evaluate(dm) - - # subclass testing diff --git a/tests/madm/test_base.py b/tests/madm/test_base.py new file mode 100644 index 0000000..9388702 --- /dev/null +++ b/tests/madm/test_base.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.madm._base.""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +import numpy as np + +from pyquery import PyQuery + +import pytest + +from skcriteria.madm import ( + KernelResult, + RankResult, + ResultABC, + SKCDecisionMakerABC, +) + +# ============================================================================= +# SKCDecisionMakerABC +# ============================================================================= + + +def test_SKCDecisionMakerABC_flow(decision_matrix): + + dm = decision_matrix(seed=42) + + class Foo(SKCDecisionMakerABC): + _skcriteria_parameters = [] + + def _evaluate_data(self, alternatives, **kwargs): + return np.arange(len(alternatives)) + 1, {} + + def _make_result(self, alternatives, values, extra): + return { + "alternatives": alternatives, + "rank": values, + "extra": extra, + } + + ranker = Foo() + + result = ranker.evaluate(dm) + + assert np.all(result["alternatives"] == dm.alternatives) + assert np.all(result["rank"] == np.arange(len(dm.alternatives)) + 1) + assert result["extra"] == {} + + +@pytest.mark.parametrize("not_redefine", ["_evaluate_data", "_make_result"]) +def test_SKCDecisionMakerABC_not_redefined(not_redefine): + content = {"_skcriteria_parameters": []} + for method_name in ["_evaluate_data", "_make_result", "_validate_data"]: + if method_name != not_redefine: + content[method_name] = lambda **kws: None + + Foo = type("Foo", (SKCDecisionMakerABC,), content) + + with pytest.raises(TypeError): + Foo() + + +def test_SKCDecisionMakerABC_evaluate_data_not_implemented(decision_matrix): + + dm = decision_matrix(seed=42) + + class Foo(SKCDecisionMakerABC): + _skcriteria_parameters = [] + + def _evaluate_data(self, **kwargs): + super()._evaluate_data(**kwargs) + + def _make_result(self, alternatives, values, extra): + return { + "alternatives": alternatives, + "rank": values, + "extra": extra, + } + + ranker = Foo() + + with pytest.raises(NotImplementedError): + ranker.evaluate(dm) + + +def test_SKCDecisionMakerABC_make_result_not_implemented(decision_matrix): + + dm = decision_matrix(seed=42) + + class Foo(SKCDecisionMakerABC): + _skcriteria_parameters = [] + + def _evaluate_data(self, alternatives, **kwargs): + return np.arange(len(alternatives)) + 1, {} + + def _make_result(self, **kwargs): + super()._make_result(**kwargs) + + ranker = Foo() + + with pytest.raises(NotImplementedError): + ranker.evaluate(dm) + + +# ============================================================================= +# RESULT BASE +# ============================================================================= + + +class test_ResultBase_skacriteria_result_column_no_defined: + + with pytest.raises(TypeError): + + class Foo(ResultABC): + def _validate_result(self, values): + pass + + +class test_ResultBase_original_validare_result_fail: + class Foo(ResultABC): + _skcriteria_result_column = "foo" + + def _validate_result(self, values): + return super()._validate_result(values) + + with pytest.raises(NotImplementedError): + Foo("foo", ["abc"], [1, 2, 3], {}) + + +# ============================================================================= +# RANK RESULT +# ============================================================================= + + +def test_RankResult(): + method = "foo" + alternatives = ["a", "b", "c"] + rank = [1, 2, 3] + extra = {"alfa": 1} + + result = RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + ) + + assert np.all(result.method == method) + assert np.all(result.alternatives == alternatives) + assert np.all(result.rank_ == rank) + assert np.all(result.extra_ == result.e_ == extra) + + +@pytest.mark.parametrize("rank", [[1, 2, 5], [1, 1, 1], [1, 2, 2], [1, 2]]) +def test_RankResult_invalid_rank(rank): + method = "foo" + alternatives = ["a", "b", "c"] + extra = {"alfa": 1} + + with pytest.raises(ValueError): + RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + ) + + +def test_RankResult_shape(): + random = np.random.default_rng(seed=42) + length = random.integers(10, 100) + + rank = np.arange(length) + 1 + alternatives = [f"A.{r}" for r in rank] + method = "foo" + extra = {} + + result = RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + ) + + assert result.shape == (length, 1) + + +def test_RankResult_len(): + random = np.random.default_rng(seed=42) + length = random.integers(10, 100) + + rank = np.arange(length) + 1 + alternatives = [f"A.{r}" for r in rank] + method = "foo" + extra = {} + + result = RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + ) + + assert len(result) == length + + +def test_RankResult_repr(): + method = "foo" + alternatives = ["a", "b", "c"] + rank = [1, 2, 3] + extra = {"alfa": 1} + + result = RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + ) + + expected = " a b c\n" "Rank 1 2 3\n" "[Method: foo]" + + assert repr(result) == expected + + +def test_RankResult_repr_html(): + method = "foo" + alternatives = ["a", "b", "c"] + rank = [1, 2, 3] + extra = {"alfa": 1} + + result = PyQuery( + RankResult( + method=method, alternatives=alternatives, values=rank, extra=extra + )._repr_html_() + ) + + expected = PyQuery( + """ +
+ + + + + + + + + + + + + + + + + +
abc
+ Rank + 123
+ Method: foo +
+ """ + ) + assert result.remove("style").text() == expected.remove("style").text() + + +# ============================================================================= +# KERNEL +# ============================================================================= + + +@pytest.mark.parametrize("values", [[1, 2, 5], [True, False, 1], [1, 2, 3]]) +def test_KernelResult_invalid_rank(values): + method = "foo" + alternatives = ["a", "b", "c"] + extra = {"alfa": 1} + + with pytest.raises(ValueError): + KernelResult( + method=method, + alternatives=alternatives, + values=values, + extra=extra, + ) + + +def test_KernelResult_repr_html(): + method = "foo" + alternatives = ["a", "b", "c"] + rank = [True, False, True] + extra = {"alfa": 1} + + result = PyQuery( + KernelResult( + method=method, alternatives=alternatives, values=rank, extra=extra + )._repr_html_() + ) + + expected = PyQuery( + """ +
+ + + + + + + + + + + + + + + + + +
abc
+ Kernel + TrueFalseTrue
+ Method: foo +
+ """ + ) + + assert result.remove("style").text() == expected.remove("style").text() diff --git a/tests/madm/test_moora.py b/tests/madm/test_moora.py index a764501..a976fde 100644 --- a/tests/madm/test_moora.py +++ b/tests/madm/test_moora.py @@ -21,7 +21,7 @@ import pytest import skcriteria -from skcriteria.core import RankResult +from skcriteria.madm import RankResult from skcriteria.madm.moora import ( FullMultiplicativeForm, MultiMOORA, diff --git a/tests/madm/test_similarity.py b/tests/madm/test_similarity.py index 00aae7c..914e7e3 100644 --- a/tests/madm/test_similarity.py +++ b/tests/madm/test_similarity.py @@ -21,7 +21,7 @@ import pytest import skcriteria -from skcriteria.core import RankResult +from skcriteria.madm import RankResult from skcriteria.madm.similarity import TOPSIS from skcriteria.preprocessing.scalers import VectorScaler diff --git a/tests/madm/test_simple.py b/tests/madm/test_simple.py index c516dce..6131b88 100644 --- a/tests/madm/test_simple.py +++ b/tests/madm/test_simple.py @@ -23,7 +23,7 @@ import pytest import skcriteria -from skcriteria.core import RankResult +from skcriteria.madm import RankResult from skcriteria.madm.simple import WeightedProductModel, WeightedSumModel from skcriteria.preprocessing.invert_objectives import MinimizeToMaximize from skcriteria.preprocessing.scalers import SumScaler diff --git a/tests/madm/test_simus.py b/tests/madm/test_simus.py index 3ecea97..62465a5 100644 --- a/tests/madm/test_simus.py +++ b/tests/madm/test_simus.py @@ -21,7 +21,7 @@ import pytest import skcriteria -from skcriteria.core import RankResult +from skcriteria.madm import RankResult from skcriteria.madm.simus import SIMUS # ============================================================================= diff --git a/tests/preprocessing/test_weighters.py b/tests/preprocessing/test_weighters.py index b5f9498..2046b8d 100644 --- a/tests/preprocessing/test_weighters.py +++ b/tests/preprocessing/test_weighters.py @@ -29,12 +29,68 @@ Critic, EntropyWeighter, EqualWeighter, + SKCWeighterABC, StdWeighter, critic_weights, pearson_correlation, spearman_correlation, ) + +# ============================================================================= +# WEIGHTER +# ============================================================================= + + +def test_SKCWeighterABC_weight_matrix_not_implemented(decision_matrix): + class Foo(SKCWeighterABC): + _skcriteria_parameters = [] + + def _weight_matrix(self, **kwargs): + return super()._weight_matrix(**kwargs) + + transformer = Foo() + dm = decision_matrix(seed=42) + + with pytest.raises(NotImplementedError): + transformer.transform(dm) + + +def test_SKCWeighterABC_not_redefined_abc_methods(): + class Foo(SKCWeighterABC): + _skcriteria_parameters = [] + + with pytest.raises(TypeError): + Foo() + + +def test_SKCWeighterABC_flow(decision_matrix): + + dm = decision_matrix(seed=42) + expected_weights = np.ones(dm.matrix.shape[1]) * 42 + + class Foo(SKCWeighterABC): + _skcriteria_parameters = [] + + def _weight_matrix(self, matrix, **kwargs): + return expected_weights + + transformer = Foo() + + expected = skcriteria.mkdm( + matrix=dm.matrix, + objectives=dm.objectives, + weights=expected_weights, + dtypes=dm.dtypes, + alternatives=dm.alternatives, + criteria=dm.criteria, + ) + + result = transformer.transform(dm) + + assert result.equals(expected) + + # ============================================================================= # TEST EQUAL WEIGHTERS # ============================================================================= From 0c8deba59f6a6a601b6bf6234a01d6a1f6fd3ed1 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 19:41:01 -0300 Subject: [PATCH 22/47] coverage fix --- tests/core/test_methods.py | 48 +++++++++++++++++++++++++++++--------- tox.ini | 3 ++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 36d76ac..0f1d956 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -21,6 +21,7 @@ import pytest from skcriteria.core import methods +from skcriteria.pipeline import SKCPipeline # ============================================================================= @@ -234,27 +235,52 @@ def _get_subclasses(cls): if not is_abstract: yield cls - for subc in cls.__subclasses__(): - for subsub in _get_subclasses(subc): - yield subsub + for subc in cls.__subclasses__(): + for subsub in _get_subclasses(subc): + yield subsub + + +class _FakeTrans: + def transform(self): + pass + + def __eq__(self, o): + return isinstance(o, _FakeTrans) + + +class _FakeDM: + def evaluate(self): + pass + + def __eq__(self, o): + return isinstance(o, _FakeDM) + + +_extra_parameters_by_type = { + methods.SKCMatrixAndWeightTransformerABC: {"target": "both"}, + SKCPipeline: {"steps": [("trans", _FakeTrans()), ("dm", _FakeDM())]}, +} @pytest.mark.run(order=-1) def test_SLCMethodABC_concrete_subclass_copy(): - extra_parameters_by_type = { - methods.SKCMatrixAndWeightTransformerABC: {"target": "both"} - } - for scls in _get_subclasses(methods.SKCMethodABC): + print(scls) + kwargs = {} - for cls, extra_params in extra_parameters_by_type.items(): + for cls, extra_params in _extra_parameters_by_type.items(): if issubclass(scls, cls): kwargs.update(extra_params) original = scls(**kwargs) copy = original.copy() - assert ( - original.get_parameters() == copy.get_parameters() - ), f"'{scls.__qualname__}' instance not correctly copied." + try: + assert ( + original.get_parameters() == copy.get_parameters() + ), f"'{scls.__qualname__}' instance not correctly copied." + except: + import ipdb + + ipdb.set_trace() diff --git a/tox.ini b/tox.ini index 9aa0fab..169a3b2 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ envlist = deps = ipdb pytest + pytest-ordering usedevelop = True commands = pytest tests/ {posargs} @@ -43,7 +44,7 @@ deps = pytest-cov commands = - coverage erase - - pytest -q tests/ --cov=skcriteria --cov-append --cov-report= + - pytest -q tests/ --cov=skcriteria --cov-append --cov-report= -vv coverage report --fail-under=100 -m From d9cf5972121ef60a7f10cbed20ef9ac0b13c6e0e Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 1 Feb 2022 19:41:13 -0300 Subject: [PATCH 23/47] coverage fix --- docs/source/api/madm/_base.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/source/api/madm/_base.rst diff --git a/docs/source/api/madm/_base.rst b/docs/source/api/madm/_base.rst new file mode 100644 index 0000000..93cd101 --- /dev/null +++ b/docs/source/api/madm/_base.rst @@ -0,0 +1,7 @@ +``skcriteria.madm._base`` module +================================== + +.. automodule:: skcriteria.madm._base + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file From e35a7336494e44976570084317979f09885d8b57 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 3 Feb 2022 00:23:04 -0300 Subject: [PATCH 24/47] filters tested --- skcriteria/madm/_base.py | 1 - skcriteria/pipeline.py | 8 +- skcriteria/preprocessing/filters.py | 212 ++++++++++ tests/core/test_methods.py | 61 --- tests/core/test_methods_extensions.py | 94 +++++ tests/preprocessing/test_filters.py | 557 ++++++++++++++++++++++++++ tox.ini | 2 +- 7 files changed, 871 insertions(+), 64 deletions(-) create mode 100644 skcriteria/preprocessing/filters.py create mode 100644 tests/core/test_methods_extensions.py create mode 100644 tests/preprocessing/test_filters.py diff --git a/skcriteria/madm/_base.py b/skcriteria/madm/_base.py index c786f47..c8c25d2 100644 --- a/skcriteria/madm/_base.py +++ b/skcriteria/madm/_base.py @@ -34,7 +34,6 @@ class SKCDecisionMakerABC(SKCMethodABC): """Mixin class for all decisor based methods in scikit-criteria.""" _skcriteria_abstract_class = True - _skcriteria_dm_type = "decision_maker" @abc.abstractmethod diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index a152937..49a5519 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -57,8 +57,13 @@ class SKCPipeline(SKCMethodABC): _skcriteria_parameters = ["steps"] def __init__(self, steps): + steps = list(steps) self._validate_steps(steps) - self.steps = list(steps) + self._steps = steps + + @property + def steps(self): + return list(self._steps) def __len__(self): """Return the length of the Pipeline.""" @@ -150,6 +155,7 @@ def transform(self, dm): return dm + # ============================================================================= # FUNCTIONS # ============================================================================= diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py new file mode 100644 index 0000000..fbed88f --- /dev/null +++ b/skcriteria/preprocessing/filters.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Normalization through the distance to distance function.""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +import abc +from collections.abc import Collection +import functools + +import numpy as np + +from ..core import SKCTransformerABC +from ..utils import doc_inherit + +# ============================================================================= +# BASE CLASS +# ============================================================================= + + +class SKCFilterABC(SKCTransformerABC): + + _skcriteria_parameters = frozenset(["criteria_filters"]) + _skcriteria_abstract_class = True + + def __init__(self, criteria_filters): + if not len(criteria_filters): + raise ValueError("Must provide at least one filter") + + criteria_filters = dict(criteria_filters) + self._validate_filters(criteria_filters) + self._criteria_filters = criteria_filters + + @property + def criteria_filters(self): + return dict(self._criteria_filters) + + @abc.abstractmethod + def _validate_filters(self, filters): + raise NotImplementedError() + + @abc.abstractmethod + def _make_mask(self, matrix, criteria): + raise NotImplementedError() + + def _transform_data(self, matrix, criteria, alternatives, **kwargs): + criteria_not_found = set(self._criteria_filters).difference(criteria) + if criteria_not_found: + raise ValueError(f"Missing criteria: {criteria_not_found}") + + mask = self._make_mask(matrix, criteria) + + filtered_matrix = matrix[mask] + filtered_alternatives = alternatives[mask] + + kwargs.update( + matrix=filtered_matrix, + criteria=criteria, + alternatives=filtered_alternatives, + dtypes=None, + ) + return kwargs + + +# ============================================================================= +# GENERIC FILTER +# ============================================================================= + + +class Filter(SKCFilterABC): + def _validate_filters(self, filters): + for filter_name, filter_value in filters.items(): + if not isinstance(filter_name, str): + raise ValueError("All filter keys must be instance of 'str'") + if not callable(filter_value): + raise ValueError("All filter values must be callable") + + def _make_mask(self, matrix, criteria): + mask_list = [] + for fname, fvalue in self._criteria_filters.items(): + crit_idx = np.in1d(criteria, fname, assume_unique=False) + crit_array = matrix[:, crit_idx].flatten() + crit_mask = np.apply_along_axis(fvalue, axis=0, arr=crit_array) + mask_list.append(crit_mask) + + mask = np.all(np.column_stack(mask_list), axis=1) + + return mask + + +# ============================================================================= +# ARITHMETIC FILTER +# ============================================================================= + + +class SKCArithmeticFilterABC(SKCFilterABC): + _skcriteria_abstract_class = True + + @abc.abstractmethod + def _filter(self, arr, cond): + raise NotImplementedError() + + def _validate_filters(self, filters): + for filter_name, filter_value in filters.items(): + if not isinstance(filter_name, str): + raise ValueError("All filter keys must be instance of 'str'") + if not isinstance(filter_value, (int, float, complex, np.number)): + raise ValueError( + "All filter values must be some kind of number" + ) + + def _make_mask(self, matrix, criteria): + filter_names, filter_values = [], [] + + for fname, fvalue in self._criteria_filters.items(): + filter_names.append(fname) + filter_values.append(fvalue) + + idxs = np.in1d(criteria, filter_names) + matrix = matrix[:, idxs] + mask = np.all(self._filter(matrix, filter_values), axis=1) + + return mask + + +class FilterGT(SKCArithmeticFilterABC): + + _filter = np.greater + + +class FilterGE(SKCArithmeticFilterABC): + + _filter = np.greater_equal + + +class FilterLT(SKCArithmeticFilterABC): + + _filter = np.less + + +class FilterLE(SKCArithmeticFilterABC): + + _filter = np.less_equal + + +class FilterEQ(SKCArithmeticFilterABC): + + _filter = np.equal + + +class FilterNE(SKCArithmeticFilterABC): + + _filter = np.not_equal + + +# ============================================================================= +# SET FILT +# ============================================================================= + + +class SKCSetFilterABC(SKCFilterABC): + _skcriteria_abstract_class = True + + @abc.abstractmethod + def _set_filter(self, arr, cond): + raise NotImplementedError() + + def _validate_filters(self, filters): + for filter_name, filter_value in filters.items(): + if not isinstance(filter_name, str): + raise ValueError("All filter keys must be instance of 'str'") + + if not ( + isinstance(filter_value, Collection) and len(filter_value) + ): + raise ValueError( + "All filter values must be iterable with length > 1" + ) + + def _make_mask(self, matrix, criteria): + mask_list = [] + for fname, fset in self._criteria_filters.items(): + crit_idx = np.in1d(criteria, fname, assume_unique=False) + crit_array = matrix[:, crit_idx].flatten() + crit_mask = self._set_filter(crit_array, fset) + mask_list.append(crit_mask) + + mask = np.all(np.column_stack(mask_list), axis=1) + + return mask + + +class FilterIn(SKCSetFilterABC): + def _set_filter(self, arr, cond): + return np.isin(arr, cond) + + +class FilterNotIn(SKCSetFilterABC): + def _set_filter(self, arr, cond): + return np.isin(arr, cond, invert=True) diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 0f1d956..476904f 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -21,7 +21,6 @@ import pytest from skcriteria.core import methods -from skcriteria.pipeline import SKCPipeline # ============================================================================= @@ -224,63 +223,3 @@ def _transform_weights(self, weights): foo = Foo("both") assert foo.target == Foo._TARGET_BOTH - -# subclass testing - - -def _get_subclasses(cls): - - is_abstract = vars(cls).get("_skcriteria_abstract_class", False) - - if not is_abstract: - yield cls - - for subc in cls.__subclasses__(): - for subsub in _get_subclasses(subc): - yield subsub - - -class _FakeTrans: - def transform(self): - pass - - def __eq__(self, o): - return isinstance(o, _FakeTrans) - - -class _FakeDM: - def evaluate(self): - pass - - def __eq__(self, o): - return isinstance(o, _FakeDM) - - -_extra_parameters_by_type = { - methods.SKCMatrixAndWeightTransformerABC: {"target": "both"}, - SKCPipeline: {"steps": [("trans", _FakeTrans()), ("dm", _FakeDM())]}, -} - - -@pytest.mark.run(order=-1) -def test_SLCMethodABC_concrete_subclass_copy(): - - for scls in _get_subclasses(methods.SKCMethodABC): - print(scls) - - kwargs = {} - for cls, extra_params in _extra_parameters_by_type.items(): - if issubclass(scls, cls): - kwargs.update(extra_params) - - original = scls(**kwargs) - copy = original.copy() - - try: - assert ( - original.get_parameters() == copy.get_parameters() - ), f"'{scls.__qualname__}' instance not correctly copied." - except: - import ipdb - - ipdb.set_trace() diff --git a/tests/core/test_methods_extensions.py b/tests/core/test_methods_extensions.py new file mode 100644 index 0000000..766761e --- /dev/null +++ b/tests/core/test_methods_extensions.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.methods + +""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +from numpy import block +import pytest + +from skcriteria.core import methods +from skcriteria.pipeline import SKCPipeline +from skcriteria.preprocessing.filters import ( + Filter, + SKCArithmeticFilterABC, + SKCSetFilterABC, +) + +# ============================================================================= +# TEST UTILITES +# ============================================================================= + + +def _get_subclasses(cls): + + is_abstract = vars(cls).get("_skcriteria_abstract_class", False) + + if not is_abstract and cls.copy == methods.SKCMethodABC.copy: + yield cls + + for subc in cls.__subclasses__(): + for subsub in _get_subclasses(subc): + yield subsub + + +class _FakeTrans: + def transform(self): + pass + + def __eq__(self, o): + return isinstance(o, _FakeTrans) + + +class _FakeDM: + def evaluate(self): + pass + + def __eq__(self, o): + return isinstance(o, _FakeDM) + + +# Some methods need extra parameters. +_extra_parameters_by_type = { + methods.SKCMatrixAndWeightTransformerABC: {"target": "both"}, + SKCPipeline: {"steps": [("trans", _FakeTrans()), ("dm", _FakeDM())]}, + Filter: {"criteria_filters": {"foo": lambda e: e}}, + SKCArithmeticFilterABC: {"criteria_filters": {"foo": 1}}, + SKCSetFilterABC: {"criteria_filters": {"foo": [1]}}, +} + +# ============================================================================= +# TEST COPY +# ============================================================================= + + +@pytest.mark.run(order=-1) +def test_SLCMethodABC_concrete_subclass_copy(): + + for scls in _get_subclasses(methods.SKCMethodABC): + + kwargs = {} + for cls, extra_params in _extra_parameters_by_type.items(): + if issubclass(scls, cls): + kwargs.update(extra_params) + + original = scls(**kwargs) + copy = original.copy() + + assert ( + original.get_parameters() == copy.get_parameters() + ), f"'{scls.__qualname__}' instance not correctly copied." diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py new file mode 100644 index 0000000..7908fa9 --- /dev/null +++ b/tests/preprocessing/test_filters.py @@ -0,0 +1,557 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.preprocessing.filters. + +""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +import numpy as np +import pytest + +import skcriteria as skc +from skcriteria.preprocessing import filters + + +# ============================================================================= +# TEST CLASSES +# ============================================================================= + + +def test_SKCFilterABC_not_provide_filters(): + class FooFilter(filters.SKCFilterABC): + def _make_mask(self, matrix, criteria): + pass + + def _validate_filters(self, filters): + pass + + with pytest.raises(ValueError): + FooFilter({}) + + +def test_SKCFilterABC_not_implemented_make_mask(): + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + class FooFilter(filters.SKCFilterABC): + def _make_mask(self, matrix, criteria): + return super()._make_mask(matrix, criteria) + + def _validate_filters(self, filters): + pass + + tfm = FooFilter({"ROE": 1}) + + with pytest.raises(NotImplementedError): + tfm.transform(dm) + + +def test_SKCFilterABC_not_implemented_validate_filters(): + class FooFilter(filters.SKCFilterABC): + def _make_mask(self, matrix, criteria): + pass + + def _validate_filters(self, filters): + return super()._validate_filters(filters) + + with pytest.raises(NotImplementedError): + FooFilter({"ROE": 1}) + + +def test_SKCFilterABC_missing_criteria(): + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + class FooFilter(filters.SKCFilterABC): + def _make_mask(self, matrix, criteria): + pass + + def _validate_filters(self, filters): + pass + + tfm = FooFilter({"ZARAZA": 1}) + + with pytest.raises(ValueError): + tfm.transform(dm) + + +# ============================================================================= +# FILTER +# ============================================================================= + + +def test_Filter_criteria_is_not_str(): + + with pytest.raises(ValueError): + filters.Filter({1: lambda e: e > 1}) + + +def test_Filter_filter_is_not_callable(): + + with pytest.raises(ValueError): + filters.Filter({"foo": 2}) + + +def test_Filter(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 6, 28], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "AA", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.Filter( + { + "ROE": lambda e: e > 1, + "RI": lambda e: e >= 28, + } + ) + + result = tfm.transform(dm) + assert result.equals(expected) + + +# ============================================================================= +# ARITHMETIC FILTER +# ============================================================================= + + +def test_SKCArithmeticFilterABC_not_implemented__filter(): + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + class FooFilter(filters.SKCArithmeticFilterABC): + def _filter(self, arr, cond): + return super()._filter(arr, cond) + + tfm = FooFilter({"ROE": 1}) + + with pytest.raises(NotImplementedError): + tfm.transform(dm) + + +def test_SKCArithmeticFilterABC_criteria_is_not_str(): + class FooFilter(filters.SKCArithmeticFilterABC): + _filter = np.greater + + with pytest.raises(ValueError): + FooFilter({1: 1}) + + +def test_SKCArithmeticFilterABC_filter_is_not_a_number(): + class FooFilter(filters.SKCArithmeticFilterABC): + _filter = np.greater + + with pytest.raises(ValueError): + FooFilter({"foo": "nope"}) + + +def test_FilterGT(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 6, 28], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "AA", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterGT({"ROE": 1, "RI": 27}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterGE(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 6, 28], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "AA", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterGE({"ROE": 2, "RI": 28}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterGE(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 6, 28], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "AA", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterGE({"ROE": 2, "RI": 28}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterLT(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [5, 4, 26], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["JN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterLT({"ROE": 7, "CAP": 5}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterLE(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [5, 4, 26], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["JN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterLE({"ROE": 5, "CAP": 4}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterEQ(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [5, 4, 26], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["JN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterEQ({"ROE": 5, "CAP": 4}) + + result = tfm.transform(dm) + assert result.equals(expected) + + +def test_FilterNE(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [1, 7, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "MM"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterNE({"ROE": 5, "CAP": 4}) + + result = tfm.transform(dm) + + assert result.equals(expected) + + +# ============================================================================= +# SET FILTER +# ============================================================================= + +def test_SKCSetFilterABC_not_implemented__set_filter(): + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + class FooFilter(filters.SKCSetFilterABC): + def _set_filter(self, arr, cond): + return super()._set_filter(arr, cond) + + tfm = FooFilter({"ROE": [1]}) + + with pytest.raises(NotImplementedError): + tfm.transform(dm) + +def test_SKCSetFilterABC_criteria_is_not_str(): + class FooFilter(filters.SKCSetFilterABC): + def _set_filter(self, arr, cond): + pass + + with pytest.raises(ValueError): + FooFilter({1: [1]}) + + +def test_SKCSetFilterABC_filter_is_not_a_number(): + class FooFilter(filters.SKCSetFilterABC): + def _set_filter(self, arr, cond): + pass + + with pytest.raises(ValueError): + FooFilter({"foo": 1}) + + +def test_FilterIn(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [5, 4, 26], + [1, 7, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["JN", "MM"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterIn({"ROE": [5, 1], "CAP": [4, 7]}) + + result = tfm.transform(dm) + + assert result.equals(expected) + + +def test_FilterNotIn(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterNotIn({"ROE": [5, 1], "CAP": [4, 7]}) + + result = tfm.transform(dm) + + assert result.equals(expected) diff --git a/tox.ini b/tox.ini index 169a3b2..0d8b639 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ deps = pytest-cov commands = - coverage erase - - pytest -q tests/ --cov=skcriteria --cov-append --cov-report= -vv + - pytest -q tests/ --cov=skcriteria --cov-append --cov-report= {posargs} coverage report --fail-under=100 -m From c3cb18eb0ad15af8ec72d0a79a8577710312d6f5 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 3 Feb 2022 00:30:28 -0300 Subject: [PATCH 25/47] only need to fix the documentation and then check how its works --- docs/source/api/preprocessing/filters.rst | 7 +++++ skcriteria/pipeline.py | 1 - skcriteria/preprocessing/filters.py | 2 +- tests/core/test_methods.py | 1 - tests/core/test_methods_extensions.py | 1 - tests/preprocessing/test_filters.py | 37 ++--------------------- 6 files changed, 11 insertions(+), 38 deletions(-) create mode 100644 docs/source/api/preprocessing/filters.rst diff --git a/docs/source/api/preprocessing/filters.rst b/docs/source/api/preprocessing/filters.rst new file mode 100644 index 0000000..274a832 --- /dev/null +++ b/docs/source/api/preprocessing/filters.rst @@ -0,0 +1,7 @@ +``skcriteria.preprocessing.filters`` module +============================================= + +.. automodule:: skcriteria.preprocessing.filters + :members: + :undoc-members: + :show-inheritance: diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index 49a5519..29b8675 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -155,7 +155,6 @@ def transform(self, dm): return dm - # ============================================================================= # FUNCTIONS # ============================================================================= diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index fbed88f..33cdb7b 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -18,7 +18,6 @@ import abc from collections.abc import Collection -import functools import numpy as np @@ -55,6 +54,7 @@ def _validate_filters(self, filters): def _make_mask(self, matrix, criteria): raise NotImplementedError() + @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, criteria, alternatives, **kwargs): criteria_not_found = set(self._criteria_filters).difference(criteria) if criteria_not_found: diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index 476904f..b96918a 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -222,4 +222,3 @@ def _transform_weights(self, weights): foo = Foo("both") assert foo.target == Foo._TARGET_BOTH - diff --git a/tests/core/test_methods_extensions.py b/tests/core/test_methods_extensions.py index 766761e..0e1cd18 100644 --- a/tests/core/test_methods_extensions.py +++ b/tests/core/test_methods_extensions.py @@ -18,7 +18,6 @@ # IMPORTS # ============================================================================= -from numpy import block import pytest from skcriteria.core import methods diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py index 7908fa9..1714a86 100644 --- a/tests/preprocessing/test_filters.py +++ b/tests/preprocessing/test_filters.py @@ -19,6 +19,7 @@ # ============================================================================= import numpy as np + import pytest import skcriteria as skc @@ -280,40 +281,6 @@ def test_FilterGE(): assert result.equals(expected) -def test_FilterGE(): - - dm = skc.mkdm( - matrix=[ - [7, 5, 35], - [5, 4, 26], - [5, 6, 28], - [1, 7, 30], - [5, 8, 30], - ], - objectives=[max, max, min], - weights=[2, 4, 1], - alternatives=["PE", "JN", "AA", "MM", "FN"], - criteria=["ROE", "CAP", "RI"], - ) - - expected = skc.mkdm( - matrix=[ - [7, 5, 35], - [5, 6, 28], - [5, 8, 30], - ], - objectives=[max, max, min], - weights=[2, 4, 1], - alternatives=["PE", "AA", "FN"], - criteria=["ROE", "CAP", "RI"], - ) - - tfm = filters.FilterGE({"ROE": 2, "RI": 28}) - - result = tfm.transform(dm) - assert result.equals(expected) - - def test_FilterLT(): dm = skc.mkdm( @@ -448,6 +415,7 @@ def test_FilterNE(): # SET FILTER # ============================================================================= + def test_SKCSetFilterABC_not_implemented__set_filter(): dm = skc.mkdm( matrix=[ @@ -472,6 +440,7 @@ def _set_filter(self, arr, cond): with pytest.raises(NotImplementedError): tfm.transform(dm) + def test_SKCSetFilterABC_criteria_is_not_str(): class FooFilter(filters.SKCSetFilterABC): def _set_filter(self, arr, cond): From debbf1ecaf3408803c49bb719d03b6b3095015d9 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sat, 5 Feb 2022 13:44:10 -0300 Subject: [PATCH 26/47] doc of filters done --- .gitignore | 2 +- docs/source/conf.py | 6 +- docs/source/index.rst | 2 +- skcriteria/core/methods.py | 9 +- skcriteria/madm/_base.py | 6 +- skcriteria/pipeline.py | 1 + skcriteria/preprocessing/filters.py | 420 ++++++++++++++++++++++++-- skcriteria/preprocessing/weighters.py | 4 +- skcriteria/utils/decorators.py | 18 +- skcriteria/utils/lp.py | 4 +- tests/core/test_methods.py | 14 +- tests/preprocessing/test_filters.py | 18 +- 12 files changed, 451 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index b958670..cdd5137 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,4 @@ setuptools-*.zip .pytest_cache .vscode/ result_images/ -docs/source/_dynamic/README +docs/source/_dynamic/README.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 04b6f5c..0c91cc1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -58,7 +58,11 @@ "nbsphinx", "sphinxcontrib.bibtex", ] +# ============================================================================= +# EXTRA CONF +# ============================================================================= +autodoc_member_order = "bysource" # ============================================================================= # BIB TEX @@ -241,7 +245,7 @@ readme_md = fp.read().split("")[-1] -README_RST_PATH = CURRENT_PATH / "_dynamic" / "README" +README_RST_PATH = CURRENT_PATH / "_dynamic" / "README.rst" with open(README_RST_PATH, "w") as fp: diff --git a/docs/source/index.rst b/docs/source/index.rst index e0c5162..4be4522 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,7 +16,7 @@ | .. Here we render the README.md of the repository as a main page -.. include:: _dynamic/README +.. include:: _dynamic/README.rst Contents diff --git a/skcriteria/core/methods.py b/skcriteria/core/methods.py index 06ed3c1..7974544 100644 --- a/skcriteria/core/methods.py +++ b/skcriteria/core/methods.py @@ -123,12 +123,12 @@ def copy(self, **kwargs): # ============================================================================= -# SKCTransformer MIXIN +# SKCTransformer ABC # ============================================================================= class SKCTransformerABC(SKCMethodABC): - """Mixin class for all transformer in scikit-criteria.""" + """Abstract class for all transformer in scikit-criteria.""" _skcriteria_dm_type = "transformer" _skcriteria_abstract_class = True @@ -177,11 +177,12 @@ def transform(self, dm): class SKCMatrixAndWeightTransformerABC(SKCTransformerABC): """Transform weights and matrix together or independently. - The Transformer that implements this mixin can be configured to transform + The Transformer that implements this abstract class can be configured to + transform `weights`, `matrix` or `both` so only that part of the DecisionMatrix is altered. - This mixin require to redefine ``_transform_weights`` and + This abstract class require to redefine ``_transform_weights`` and ``_transform_matrix``, instead of ``_transform_data``. """ diff --git a/skcriteria/madm/_base.py b/skcriteria/madm/_base.py index c8c25d2..8bd54d3 100644 --- a/skcriteria/madm/_base.py +++ b/skcriteria/madm/_base.py @@ -31,7 +31,7 @@ class SKCDecisionMakerABC(SKCMethodABC): - """Mixin class for all decisor based methods in scikit-criteria.""" + """Abstract class for all decisor based methods in scikit-criteria.""" _skcriteria_abstract_class = True _skcriteria_dm_type = "decision_maker" @@ -197,7 +197,7 @@ def __repr__(self): return string -@doc_inherit(ResultABC) +@doc_inherit(ResultABC, warn_class=False) class RankResult(ResultABC): """Ranking of alternatives. @@ -240,7 +240,7 @@ def _repr_html_(self): return html -@doc_inherit(ResultABC) +@doc_inherit(ResultABC, warn_class=False) class KernelResult(ResultABC): """Separates the alternatives between good (kernel) and bad. diff --git a/skcriteria/pipeline.py b/skcriteria/pipeline.py index 29b8675..9eb7a81 100644 --- a/skcriteria/pipeline.py +++ b/skcriteria/pipeline.py @@ -63,6 +63,7 @@ def __init__(self, steps): @property def steps(self): + """List of steps of the pipeline.""" return list(self._steps) def __len__(self): diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index 33cdb7b..ef25e75 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -30,24 +30,73 @@ class SKCFilterABC(SKCTransformerABC): + """Abstract class capable of filtering alternatives. - _skcriteria_parameters = frozenset(["criteria_filters"]) + This abstract class require to redefine ``_coerce_filters`` and + ``_make_mask``, instead of ``_transform_data``. + + Parameters + ---------- + criteria_filters: dict + It is a dictionary in which the key is the name of a criterion, and + the value is the filter condition. + ignore_missing_criteria: bool, default: False + If True, it is ignored if a decision matrix does not have any + particular criteria that should be filtered. + + """ + + _skcriteria_parameters = frozenset( + ["criteria_filters", "ignore_missing_criteria"] + ) _skcriteria_abstract_class = True - def __init__(self, criteria_filters): + def __init__(self, criteria_filters, *, ignore_missing_criteria=False): if not len(criteria_filters): raise ValueError("Must provide at least one filter") - - criteria_filters = dict(criteria_filters) - self._validate_filters(criteria_filters) - self._criteria_filters = criteria_filters + self._criteria, self._filters = self._coerce_filters(criteria_filters) + self._ignore_missing_criteria = bool(ignore_missing_criteria) @property def criteria_filters(self): - return dict(self._criteria_filters) + """Conditions on which the alternatives will be evaluated. + + It is a dictionary in which the key is the name of a + criterion, and the value is the filter condition. + + """ + return dict(zip(self._criteria, self._filters)) + + @property + def ignore_missing_criteria(self): + """If the value is True the filter ignores the lack of a required \ + criterion. + + If the value is False, the lack of a criterion causes the filter to + fail. + + """ + return self._ignore_missing_criteria @abc.abstractmethod - def _validate_filters(self, filters): + def _coerce_filters(self, filters): + """Validate the filters. + + Parameters + ---------- + filters: dict-like + It is a dictionary in which the key is the name of a + criterion, and the value is the filter condition. + + Returns + ------- + (criteria, filters): tuple of two elements. + The tuple contains two iterables: + + 1. The first is the list of criteria. + 2. The second is the filters. + + """ raise NotImplementedError() @abc.abstractmethod @@ -56,9 +105,11 @@ def _make_mask(self, matrix, criteria): @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, criteria, alternatives, **kwargs): - criteria_not_found = set(self._criteria_filters).difference(criteria) - if criteria_not_found: + criteria_not_found = set(self._criteria).difference(criteria) + if not self._ignore_missing_criteria and criteria_not_found: raise ValueError(f"Missing criteria: {criteria_not_found}") + elif criteria_not_found: + raise ValueError() mask = self._make_mask(matrix, criteria) @@ -79,17 +130,60 @@ def _transform_data(self, matrix, criteria, alternatives, **kwargs): # ============================================================================= +@doc_inherit(SKCFilterABC, warn_class=False) class Filter(SKCFilterABC): - def _validate_filters(self, filters): + """Function based filter. + + This class accepts as a filter any arbitrary function that receives as a + parameter a as a parameter a criterion and returns a mask of the same size + as the number of the number of alternatives in the decision matrix. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.Filter({ + ... "ROE": lambda e: e > 1, + ... "RI": lambda e: e >= 28, + ... }) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + PE 7 5 35 + AA 5 6 28 + FN 5 8 30 + [3 Alternatives x 3 Criteria] + + """ + + def _coerce_filters(self, filters): + criteria, criteria_filters = [], [] for filter_name, filter_value in filters.items(): if not isinstance(filter_name, str): raise ValueError("All filter keys must be instance of 'str'") if not callable(filter_value): raise ValueError("All filter values must be callable") + criteria.append(filter_name) + criteria_filters.append(filter_value) + return tuple(criteria), tuple(criteria_filters) def _make_mask(self, matrix, criteria): mask_list = [] - for fname, fvalue in self._criteria_filters.items(): + for fname, fvalue in self.criteria_filters.items(): crit_idx = np.in1d(criteria, fname, assume_unique=False) crit_array = matrix[:, crit_idx].flatten() crit_mask = np.apply_along_axis(fvalue, axis=0, arr=crit_array) @@ -105,14 +199,32 @@ def _make_mask(self, matrix, criteria): # ============================================================================= +@doc_inherit(SKCFilterABC, warn_class=False) class SKCArithmeticFilterABC(SKCFilterABC): + """Provide a common behavior to make filters based on the same comparator. + + This abstract class require to redefine ``_filter`` method, and this will + apply to each criteria separately. + + This class is designed to implement in general arithmetic comparisons of + "==", "!=", ">", ">=", "<", "<=" taking advantage of the functions + provided by numpy (e.g. ``np.greater_equal()``). + + Notes + ----- + The filter implemented with this class are slightly faster than + function-based filters. + + """ + _skcriteria_abstract_class = True @abc.abstractmethod def _filter(self, arr, cond): raise NotImplementedError() - def _validate_filters(self, filters): + def _coerce_filters(self, filters): + criteria, criteria_filters = [], [] for filter_name, filter_value in filters.items(): if not isinstance(filter_name, str): raise ValueError("All filter keys must be instance of 'str'") @@ -120,13 +232,12 @@ def _validate_filters(self, filters): raise ValueError( "All filter values must be some kind of number" ) + criteria.append(filter_name) + criteria_filters.append(filter_value) + return tuple(criteria), tuple(criteria_filters) def _make_mask(self, matrix, criteria): - filter_names, filter_values = [], [] - - for fname, fvalue in self._criteria_filters.items(): - filter_names.append(fname) - filter_values.append(fvalue) + filter_names, filter_values = self._criteria, self._filters idxs = np.in1d(criteria, filter_names) matrix = matrix[:, idxs] @@ -135,49 +246,249 @@ def _make_mask(self, matrix, criteria): return mask +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterGT(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are greater than a \ + value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterGT({"ROE": 1, "RI": 27}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + PE 7 5 35 + AA 5 6 28 + FN 5 8 30 + [3 Alternatives x 3 Criteria] + + """ _filter = np.greater +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterGE(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are greater or \ + equal than a value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterGE({"ROE": 1, "RI": 27}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + PE 7 5 35 + AA 5 6 28 + MM 1 7 30 + FN 5 8 30 + [4 Alternatives x 3 Criteria] + + """ _filter = np.greater_equal +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterLT(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are less than a \ + value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterLT({"RI": 28}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + JN 5 4 26 + [1 Alternatives x 3 Criteria] + + """ _filter = np.less +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterLE(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are less or equal \ + than a value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterLE({"RI": 28}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + JN 5 4 26 + AA 5 6 28 + [2 Alternatives x 3 Criteria] + + """ _filter = np.less_equal +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterEQ(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are equal than a \ + value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterEQ({"CAP": 7, "RI": 30}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + MM 1 7 30 + [1 Alternatives x 3 Criteria] + + """ _filter = np.equal +@doc_inherit(SKCArithmeticFilterABC, warn_class=False) class FilterNE(SKCArithmeticFilterABC): + """Keeps the alternatives for which the criteria value are not equal than \ + a value. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterNE({"CAP": 7, "RI": 30}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + PE 7 5 35 + JN 5 4 26 + AA 5 6 28 + [3 Alternatives x 3 Criteria] + + """ _filter = np.not_equal # ============================================================================= -# SET FILT +# SET FILTERS # ============================================================================= +@doc_inherit(SKCFilterABC, warn_class=False) class SKCSetFilterABC(SKCFilterABC): + """Provide a common behavior to make filters based on set operatopms. + + This abstract class require to redefine ``_set_filter`` method, and this + will apply to each criteria separately. + + This class is designed to implement in general set comparision like + "inclusion" and "exclusion". + + """ + _skcriteria_abstract_class = True @abc.abstractmethod def _set_filter(self, arr, cond): raise NotImplementedError() - def _validate_filters(self, filters): + def _coerce_filters(self, filters): + criteria, criteria_filters = [], [] for filter_name, filter_value in filters.items(): if not isinstance(filter_name, str): raise ValueError("All filter keys must be instance of 'str'") @@ -188,10 +499,13 @@ def _validate_filters(self, filters): raise ValueError( "All filter values must be iterable with length > 1" ) + criteria.append(filter_name) + criteria_filters.append(np.asarray(filter_value)) + return criteria, criteria_filters def _make_mask(self, matrix, criteria): mask_list = [] - for fname, fset in self._criteria_filters.items(): + for fname, fset in self.criteria_filters.items(): crit_idx = np.in1d(criteria, fname, assume_unique=False) crit_array = matrix[:, crit_idx].flatten() crit_mask = self._set_filter(crit_array, fset) @@ -202,11 +516,75 @@ def _make_mask(self, matrix, criteria): return mask +@doc_inherit(SKCSetFilterABC, warn_class=False) class FilterIn(SKCSetFilterABC): + """Keeps the alternatives for which the criteria value are included in a \ + set of values. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterIn({"ROE": [7, 1], "RI": [30, 35]}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + PE 7 5 35 + MM 1 7 30 + [2 Alternatives x 3 Criteria] + + """ + def _set_filter(self, arr, cond): return np.isin(arr, cond) +@doc_inherit(SKCSetFilterABC, warn_class=False) class FilterNotIn(SKCSetFilterABC): + """Keeps the alternatives for which the criteria value are not included \ + in a set of values. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterNotIn({"ROE": [7, 1], "RI": [30, 35]}) + >>> tfm.transform(dm) + ROE[▲ 2.0] CAP[▲ 4.0] RI[▼ 1.0] + JN 5 4 26 + AA 5 6 28 + [2 Alternatives x 3 Criteria] + + """ + def _set_filter(self, arr, cond): return np.isin(arr, cond, invert=True) diff --git a/skcriteria/preprocessing/weighters.py b/skcriteria/preprocessing/weighters.py index 0c4a7eb..a280a9d 100644 --- a/skcriteria/preprocessing/weighters.py +++ b/skcriteria/preprocessing/weighters.py @@ -35,9 +35,9 @@ class SKCWeighterABC(SKCTransformerABC): - """Mixin capable of determine the weights of the matrix. + """Abstract class capable of determine the weights of the matrix. - This mixin require to redefine ``_weight_matrix``, instead of + This abstract class require to redefine ``_weight_matrix``, instead of ``_transform_data``. """ diff --git a/skcriteria/utils/decorators.py b/skcriteria/utils/decorators.py index 7aa58b9..6d297c4 100644 --- a/skcriteria/utils/decorators.py +++ b/skcriteria/utils/decorators.py @@ -14,6 +14,10 @@ # ============================================================================= # IMPORTS # ============================================================================= + +import warnings +from inspect import isclass + from custom_inherit import doc_inherit as _doc_inherit from deprecated import deprecated as _deprecated @@ -23,7 +27,7 @@ # ============================================================================= -def doc_inherit(parent): +def doc_inherit(parent, warn_class=True): """Inherit the 'parent' docstring. Returns a function/method decorator that, given parent, updates @@ -45,7 +49,17 @@ def doc_inherit(parent): """ - return _doc_inherit(parent, style="numpy") + + def _wrapper(obj): + if isclass(obj) and warn_class: + warnings.warn( + f"{obj} is a class, check if the " + "documentation was inherited properly " + ) + dec = _doc_inherit(parent, style="numpy") + return dec(obj) + + return _wrapper # ============================================================================= diff --git a/skcriteria/utils/lp.py b/skcriteria/utils/lp.py index 85fc3da..a0ea501 100644 --- a/skcriteria/utils/lp.py +++ b/skcriteria/utils/lp.py @@ -250,14 +250,14 @@ def solve(self): # ============================================================================= -@doc_inherit(_LPBase) +@doc_inherit(_LPBase, warn_class=False) class Minimize(_LPBase): """Creates a Minimize LP problem with a way better sintax than PuLP.""" sense = pulp.LpMinimize -@doc_inherit(_LPBase) +@doc_inherit(_LPBase, warn_class=False) class Maximize(_LPBase): """Creates a Maximize LP problem with a way better sintax than PuLP.""" diff --git a/tests/core/test_methods.py b/tests/core/test_methods.py index b96918a..8e5ab56 100644 --- a/tests/core/test_methods.py +++ b/tests/core/test_methods.py @@ -114,7 +114,7 @@ def __init__(self): # ============================================================================= -def test_SKCTransformerMixin_not_redefined_abc_methods(): +def test_SKCTransformerABC_not_redefined_abc_methods(): class Foo(methods.SKCTransformerABC): _skcriteria_parameters = [] @@ -127,7 +127,7 @@ class Foo(methods.SKCTransformerABC): # ============================================================================= -def test_SKCMatrixAndWeightTransformerMixin_transform_data_not_implemented( +def test_SKCMatrixAndWeightTransformerABC_transform_data_not_implemented( decision_matrix, ): class Foo(methods.SKCTransformerABC): @@ -143,7 +143,7 @@ def _transform_data(self, **kwargs): transformer.transform(dm) -def test_SKCMatrixAndWeightTransformerMixin_not_redefined_abc_methods(): +def test_SKCMatrixAndWeightTransformerABC_not_redefined_abc_methods(): class Foo(methods.SKCMatrixAndWeightTransformerABC): pass @@ -157,7 +157,7 @@ class Foo(methods.SKCMatrixAndWeightTransformerABC): Foo("both") -def test_SKCMatrixAndWeightTransformerMixin_bad_normalize_for(): +def test_SKCMatrixAndWeightTransformerABC_bad_normalize_for(): class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): ... @@ -169,7 +169,7 @@ def _transform_weights(self, weights): Foo("mtx") -def test_SKCMatrixAndWeightTransformerMixin_transform_weights_not_implemented( +def test_SKCMatrixAndWeightTransformerABC_transform_weights_not_implemented( decision_matrix, ): class Foo(methods.SKCMatrixAndWeightTransformerABC): @@ -186,7 +186,7 @@ def _transform_weights(self, weights): transformer.transform(dm) -def test_SKCMatrixAndWeightTransformerMixin_transform_weight_not_implemented( +def test_SKCMatrixAndWeightTransformerABC_transform_weight_not_implemented( decision_matrix, ): class Foo(methods.SKCMatrixAndWeightTransformerABC): @@ -203,7 +203,7 @@ def _transform_weights(self, weights): transformer.transform(dm) -def test_SKCMatrixAndWeightTransformerMixin_target(): +def test_SKCMatrixAndWeightTransformerABC_target(): class Foo(methods.SKCMatrixAndWeightTransformerABC): def _transform_matrix(self, matrix): ... diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py index 1714a86..5c02485 100644 --- a/tests/preprocessing/test_filters.py +++ b/tests/preprocessing/test_filters.py @@ -36,8 +36,8 @@ class FooFilter(filters.SKCFilterABC): def _make_mask(self, matrix, criteria): pass - def _validate_filters(self, filters): - pass + def _coerce_filters(self, filters): + return list(filters.keys()), list(filters.values()) with pytest.raises(ValueError): FooFilter({}) @@ -62,8 +62,8 @@ class FooFilter(filters.SKCFilterABC): def _make_mask(self, matrix, criteria): return super()._make_mask(matrix, criteria) - def _validate_filters(self, filters): - pass + def _coerce_filters(self, filters): + return list(filters.keys()), list(filters.values()) tfm = FooFilter({"ROE": 1}) @@ -71,13 +71,13 @@ def _validate_filters(self, filters): tfm.transform(dm) -def test_SKCFilterABC_not_implemented_validate_filters(): +def test_SKCFilterABC_not_implemented_coerce_filters(): class FooFilter(filters.SKCFilterABC): def _make_mask(self, matrix, criteria): pass - def _validate_filters(self, filters): - return super()._validate_filters(filters) + def _coerce_filters(self, filters): + return super()._coerce_filters(filters) with pytest.raises(NotImplementedError): FooFilter({"ROE": 1}) @@ -102,8 +102,8 @@ class FooFilter(filters.SKCFilterABC): def _make_mask(self, matrix, criteria): pass - def _validate_filters(self, filters): - pass + def _coerce_filters(self, filters): + return list(filters.keys()), list(filters.values()) tfm = FooFilter({"ZARAZA": 1}) From 55e8d1f2e0340c5316032e0b69f1aeaf7e02ae25 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sat, 5 Feb 2022 19:34:45 -0300 Subject: [PATCH 27/47] end filters implementation --- skcriteria/preprocessing/filters.py | 57 ++++++++++++++++++----------- skcriteria/utils/decorators.py | 4 ++ tests/preprocessing/test_filters.py | 25 ++++++++++--- tests/utils/test_decorators.py | 15 ++++++++ 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index ef25e75..7554660 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -100,21 +100,35 @@ def _coerce_filters(self, filters): raise NotImplementedError() @abc.abstractmethod - def _make_mask(self, matrix, criteria): + def _make_mask(self, matrix, criteria, criteria_to_use, criteria_filters): raise NotImplementedError() @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, criteria, alternatives, **kwargs): - criteria_not_found = set(self._criteria).difference(criteria) - if not self._ignore_missing_criteria and criteria_not_found: - raise ValueError(f"Missing criteria: {criteria_not_found}") - elif criteria_not_found: - raise ValueError() - - mask = self._make_mask(matrix, criteria) - - filtered_matrix = matrix[mask] - filtered_alternatives = alternatives[mask] + # determine which criteria defined in the filter are in the DM + criteria_to_use, criteria_filters = [], [] + for crit, flt in zip(self._criteria, self._filters): + if crit not in criteria and not self._ignore_missing_criteria: + raise ValueError(f"Missing criteria: {crit}") + elif crit in criteria: + criteria_to_use.append(crit) + criteria_filters.append(flt) + + if criteria_to_use: + + mask = self._make_mask( + matrix=matrix, + criteria=criteria, + criteria_to_use=criteria_to_use, + criteria_filters=criteria_filters, + ) + + filtered_matrix = matrix[mask] + filtered_alternatives = alternatives[mask] + + else: + filtered_matrix = matrix + filtered_alternatives = alternatives kwargs.update( matrix=filtered_matrix, @@ -181,12 +195,14 @@ def _coerce_filters(self, filters): criteria_filters.append(filter_value) return tuple(criteria), tuple(criteria_filters) - def _make_mask(self, matrix, criteria): + def _make_mask(self, matrix, criteria, criteria_to_use, criteria_filters): mask_list = [] - for fname, fvalue in self.criteria_filters.items(): - crit_idx = np.in1d(criteria, fname, assume_unique=False) + for crit_name, crit_filter in zip(criteria_to_use, criteria_filters): + crit_idx = np.in1d(criteria, crit_name, assume_unique=False) crit_array = matrix[:, crit_idx].flatten() - crit_mask = np.apply_along_axis(fvalue, axis=0, arr=crit_array) + crit_mask = np.apply_along_axis( + crit_filter, axis=0, arr=crit_array + ) mask_list.append(crit_mask) mask = np.all(np.column_stack(mask_list), axis=1) @@ -236,12 +252,11 @@ def _coerce_filters(self, filters): criteria_filters.append(filter_value) return tuple(criteria), tuple(criteria_filters) - def _make_mask(self, matrix, criteria): - filter_names, filter_values = self._criteria, self._filters + def _make_mask(self, matrix, criteria, criteria_to_use, criteria_filters): - idxs = np.in1d(criteria, filter_names) + idxs = np.in1d(criteria, criteria_to_use) matrix = matrix[:, idxs] - mask = np.all(self._filter(matrix, filter_values), axis=1) + mask = np.all(self._filter(matrix, criteria_filters), axis=1) return mask @@ -503,9 +518,9 @@ def _coerce_filters(self, filters): criteria_filters.append(np.asarray(filter_value)) return criteria, criteria_filters - def _make_mask(self, matrix, criteria): + def _make_mask(self, matrix, criteria, criteria_to_use, criteria_filters): mask_list = [] - for fname, fset in self.criteria_filters.items(): + for fname, fset in zip(criteria_to_use, criteria_filters): crit_idx = np.in1d(criteria, fname, assume_unique=False) crit_array = matrix[:, crit_idx].flatten() crit_mask = self._set_filter(crit_array, fset) diff --git a/skcriteria/utils/decorators.py b/skcriteria/utils/decorators.py index 6d297c4..27db58a 100644 --- a/skcriteria/utils/decorators.py +++ b/skcriteria/utils/decorators.py @@ -39,6 +39,10 @@ def doc_inherit(parent, warn_class=True): parent : Union[str, Any] The docstring, or object of which the docstring is utilized as the parent docstring during the docstring merge. + warn_class: bool + If it is true, and the decorated is a class, it throws a warning + since there are some issues with inheritance of documentation in + classes. Notes ----- diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py index 5c02485..71d7257 100644 --- a/tests/preprocessing/test_filters.py +++ b/tests/preprocessing/test_filters.py @@ -33,7 +33,9 @@ def test_SKCFilterABC_not_provide_filters(): class FooFilter(filters.SKCFilterABC): - def _make_mask(self, matrix, criteria): + def _make_mask( + self, matrix, criteria, criteria_to_use, criteria_filters + ): pass def _coerce_filters(self, filters): @@ -59,8 +61,12 @@ def test_SKCFilterABC_not_implemented_make_mask(): ) class FooFilter(filters.SKCFilterABC): - def _make_mask(self, matrix, criteria): - return super()._make_mask(matrix, criteria) + def _make_mask( + self, matrix, criteria, criteria_to_use, criteria_filters + ): + return super()._make_mask( + matrix, criteria, criteria_to_use, criteria_filters + ) def _coerce_filters(self, filters): return list(filters.keys()), list(filters.values()) @@ -73,7 +79,9 @@ def _coerce_filters(self, filters): def test_SKCFilterABC_not_implemented_coerce_filters(): class FooFilter(filters.SKCFilterABC): - def _make_mask(self, matrix, criteria): + def _make_mask( + self, matrix, criteria, criteria_to_use, criteria_filters + ): pass def _coerce_filters(self, filters): @@ -99,7 +107,9 @@ def test_SKCFilterABC_missing_criteria(): ) class FooFilter(filters.SKCFilterABC): - def _make_mask(self, matrix, criteria): + def _make_mask( + self, matrix, criteria, criteria_to_use, criteria_filters + ): pass def _coerce_filters(self, filters): @@ -110,6 +120,11 @@ def _coerce_filters(self, filters): with pytest.raises(ValueError): tfm.transform(dm) + tfm = FooFilter({"ZARAZA": 1}, ignore_missing_criteria=True) + + result = tfm.transform(dm) + assert result.equals(dm) and result is not dm + # ============================================================================= # FILTER diff --git a/tests/utils/test_decorators.py b/tests/utils/test_decorators.py index d597a18..422781e 100644 --- a/tests/utils/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -54,6 +54,21 @@ def func_c(): assert doc == func_a.__doc__ == func_b.__doc__ == func_c.__doc__ + # test warnings + with pytest.warns(UserWarning): + + @decorators.doc_inherit(doc, warn_class=True) + class A: # noqa + pass + + with pytest.warns(None) as warnings: + + @decorators.doc_inherit(doc, warn_class=False) + class A: # noqa + pass + + assert not warnings + def test_deprecated(): def func(): From 74761e772f3b81d5a64545576ca0a81b955024b2 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sun, 6 Feb 2022 19:46:04 -0300 Subject: [PATCH 28/47] single refactor --- skcriteria/preprocessing/filters.py | 14 +++++++------- tests/preprocessing/test_filters.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index 7554660..de75400 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -29,7 +29,7 @@ # ============================================================================= -class SKCFilterABC(SKCTransformerABC): +class SKCByCriteriaFilterABC(SKCTransformerABC): """Abstract class capable of filtering alternatives. This abstract class require to redefine ``_coerce_filters`` and @@ -144,8 +144,8 @@ def _transform_data(self, matrix, criteria, alternatives, **kwargs): # ============================================================================= -@doc_inherit(SKCFilterABC, warn_class=False) -class Filter(SKCFilterABC): +@doc_inherit(SKCByCriteriaFilterABC, warn_class=False) +class Filter(SKCByCriteriaFilterABC): """Function based filter. This class accepts as a filter any arbitrary function that receives as a @@ -215,8 +215,8 @@ def _make_mask(self, matrix, criteria, criteria_to_use, criteria_filters): # ============================================================================= -@doc_inherit(SKCFilterABC, warn_class=False) -class SKCArithmeticFilterABC(SKCFilterABC): +@doc_inherit(SKCByCriteriaFilterABC, warn_class=False) +class SKCArithmeticFilterABC(SKCByCriteriaFilterABC): """Provide a common behavior to make filters based on the same comparator. This abstract class require to redefine ``_filter`` method, and this will @@ -484,8 +484,8 @@ class FilterNE(SKCArithmeticFilterABC): # ============================================================================= -@doc_inherit(SKCFilterABC, warn_class=False) -class SKCSetFilterABC(SKCFilterABC): +@doc_inherit(SKCByCriteriaFilterABC, warn_class=False) +class SKCSetFilterABC(SKCByCriteriaFilterABC): """Provide a common behavior to make filters based on set operatopms. This abstract class require to redefine ``_set_filter`` method, and this diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py index 71d7257..40b657d 100644 --- a/tests/preprocessing/test_filters.py +++ b/tests/preprocessing/test_filters.py @@ -31,8 +31,8 @@ # ============================================================================= -def test_SKCFilterABC_not_provide_filters(): - class FooFilter(filters.SKCFilterABC): +def test_SKCByCriteriaFilterABC_not_provide_filters(): + class FooFilter(filters.SKCByCriteriaFilterABC): def _make_mask( self, matrix, criteria, criteria_to_use, criteria_filters ): @@ -45,7 +45,7 @@ def _coerce_filters(self, filters): FooFilter({}) -def test_SKCFilterABC_not_implemented_make_mask(): +def test_SKCByCriteriaFilterABC_not_implemented_make_mask(): dm = skc.mkdm( matrix=[ [7, 5, 35], @@ -60,7 +60,7 @@ def test_SKCFilterABC_not_implemented_make_mask(): criteria=["ROE", "CAP", "RI"], ) - class FooFilter(filters.SKCFilterABC): + class FooFilter(filters.SKCByCriteriaFilterABC): def _make_mask( self, matrix, criteria, criteria_to_use, criteria_filters ): @@ -77,8 +77,8 @@ def _coerce_filters(self, filters): tfm.transform(dm) -def test_SKCFilterABC_not_implemented_coerce_filters(): - class FooFilter(filters.SKCFilterABC): +def test_SKCByCriteriaFilterABC_not_implemented_coerce_filters(): + class FooFilter(filters.SKCByCriteriaFilterABC): def _make_mask( self, matrix, criteria, criteria_to_use, criteria_filters ): @@ -91,7 +91,7 @@ def _coerce_filters(self, filters): FooFilter({"ROE": 1}) -def test_SKCFilterABC_missing_criteria(): +def test_SKCByCriteriaFilterABC_missing_criteria(): dm = skc.mkdm( matrix=[ [7, 5, 35], @@ -106,7 +106,7 @@ def test_SKCFilterABC_missing_criteria(): criteria=["ROE", "CAP", "RI"], ) - class FooFilter(filters.SKCFilterABC): + class FooFilter(filters.SKCByCriteriaFilterABC): def _make_mask( self, matrix, criteria, criteria_to_use, criteria_filters ): From 773c549ec5f3e4dbae40007b376fd4ff370b3237 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 8 Feb 2022 15:16:07 -0300 Subject: [PATCH 29/47] alternatives works as accesors as parts of the matrix --- skcriteria/core/data.py | 66 ++++++++++++++++++++++++++--- skcriteria/preprocessing/filters.py | 3 +- tests/core/test_data.py | 31 ++++++++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index fb14b68..485af73 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -23,6 +23,7 @@ import enum import functools +from collections import abc import numpy as np @@ -33,7 +34,7 @@ from .plot import DecisionMatrixPlotter -from ..utils import deprecated +from ..utils import deprecated, doc_inherit # ============================================================================= @@ -115,7 +116,7 @@ def to_string(self): # ============================================================================= -# DATA CLASS +# STATS ACCESSOR # ============================================================================= @@ -213,6 +214,55 @@ def __dir__(self): ] +# ============================================================================= +# _SLICER ARRAY +# ============================================================================= +class _ACArray(np.ndarray, abc.Mapping): + """Immutable Array to provide access to the alternative and criteria \ + values. + + The behavior is the same as a numpy.ndarray but if the slice it receives + is a value contained in the array it uses an external function + to access the series with that criteria/alternative. + + Besides this it has the typical methods of a dictionary. + + """ + + def __new__(cls, input_array, skc_slicer): + obj = np.asarray(input_array).view(cls) + obj._skc_slicer = skc_slicer + return obj + + @doc_inherit(np.ndarray.__getitem__) + def __getitem__(self, k): + try: + if k in self: + return self._skc_slicer(k) + return super().__getitem__(k) + except IndexError: + raise IndexError(k) + + def __setitem__(self, k, v): + """Raise an AttributeError, this object are read-only.""" + raise AttributeError("_SlicerArray are read-only") + + @doc_inherit(abc.Mapping.items) + def items(self): + return ((e, self[e]) for e in self) + + @doc_inherit(abc.Mapping.keys) + def keys(self): + return iter(self) + + @doc_inherit(abc.Mapping.values) + def values(self): + return (self[e] for e in self) + + +# ============================================================================= +# DECISION MATRIX +# ============================================================================= class DecisionMatrix: """Representation of all data needed in the MCDA analysis. @@ -426,12 +476,16 @@ def from_mcda_data( @property def alternatives(self): """Names of the alternatives.""" - return self._data_df.index.to_numpy() + arr = self._data_df.index.to_numpy() + slicer = self._data_df.loc.__getitem__ + return _ACArray(arr, slicer) @property def criteria(self): """Names of the criteria.""" - return self._data_df.columns.to_numpy() + arr = self._data_df.columns.to_numpy() + slicer = self._data_df.__getitem__ + return _ACArray(arr, slicer) @property def weights(self): @@ -566,8 +620,8 @@ def to_dict(self): "objectives": self.iobjectives.to_numpy(), "weights": self.weights.to_numpy(), "dtypes": self.dtypes.to_numpy(), - "alternatives": self.alternatives, - "criteria": self.criteria, + "alternatives": np.asarray(self.alternatives), + "criteria": np.asarray(self.criteria), } @deprecated( diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index de75400..2a4577d 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -30,7 +30,8 @@ class SKCByCriteriaFilterABC(SKCTransformerABC): - """Abstract class capable of filtering alternatives. + """Abstract class capable of filtering alternatives based on criteria \ + values. This abstract class require to redefine ``_coerce_filters`` and ``_make_mask``, instead of ``_transform_data``. diff --git a/tests/core/test_data.py b/tests/core/test_data.py index ba435e8..ed46108 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -172,6 +172,37 @@ def test_DecisionMatrixStatsAccessor_dir(decision_matrix): assert not expected.difference(result) +# ============================================================================= +# AC ACCESSORS +# ============================================================================= + + +def test__ACArray(decision_matrix): + + content = ["a", "b", "c"] + mapping = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]} + + arr = data._ACArray(content, mapping.__getitem__) + + assert arr["a"] == [1, 2, 3] + assert arr["b"] == [4, 5, 6] + assert arr["c"] == [7, 8, 9] + + assert arr[0] == "a" + assert arr[1] == "b" + assert arr[2] == "c" + + assert dict(arr.items()) == mapping + assert list(arr.keys()) == list(arr) == content + assert sorted(list(arr.values())) == sorted(list(mapping.values())) + + with pytest.raises(AttributeError): + arr[0] = 1 + + with pytest.raises(IndexError): + arr["foo"] + + # ============================================================================= # MUST WORK # ============================================================================= From 5b77b20d7a65d1c3d0a8284630919d7c1c222fbe Mon Sep 17 00:00:00 2001 From: JuanBC Date: Tue, 8 Feb 2022 15:50:09 -0300 Subject: [PATCH 30/47] Stats splited in a new module --- docs/source/api/core/stats.rst | 7 ++ skcriteria/core/data.py | 100 +------------------------ skcriteria/core/stats.py | 111 +++++++++++++++++++++++++++ tests/core/test_stats.py | 132 +++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 99 deletions(-) create mode 100644 docs/source/api/core/stats.rst create mode 100644 skcriteria/core/stats.py create mode 100644 tests/core/test_stats.py diff --git a/docs/source/api/core/stats.rst b/docs/source/api/core/stats.rst new file mode 100644 index 0000000..37a12bc --- /dev/null +++ b/docs/source/api/core/stats.rst @@ -0,0 +1,7 @@ +``skcriteria.core.stats`` module +================================ + +.. automodule:: skcriteria.core.stats + :members: + :undoc-members: + :show-inheritance: diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 485af73..635141f 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -34,6 +34,7 @@ from .plot import DecisionMatrixPlotter +from .stats import DecisionMatrixStatsAccessor from ..utils import deprecated, doc_inherit @@ -115,105 +116,6 @@ def to_string(self): return Objective._MAX_STR.value -# ============================================================================= -# STATS ACCESSOR -# ============================================================================= - - -class DecisionMatrixStatsAccessor: - """Calculate basic statistics of the decision matrix.""" - - _DF_WHITELIST = ( - "corr", - "cov", - "describe", - "kurtosis", - "mad", - "max", - "mean", - "median", - "min", - "pct_change", - "quantile", - "sem", - "skew", - "std", - "var", - ) - - _DEFAULT_KIND = "describe" - - def __init__(self, dm): - self._dm = dm - - def __call__(self, kind=None, **kwargs): - """Calculate basic statistics of the decision matrix. - - Parameters - ---------- - kind : str - The kind of statistic to produce: - - - 'corr' : Compute pairwise correlation of columns, excluding - NA/null values. - - 'cov' : Compute pairwise covariance of columns, excluding NA/null - values. - - 'describe' : Generate descriptive statistics. - - 'kurtosis' : Return unbiased kurtosis over requested axis. - - 'mad' : Return the mean absolute deviation of the values over the - requested axis. - - 'max' : Return the maximum of the values over the requested axis. - - 'mean' : Return the mean of the values over the requested axis. - - 'median' : Return the median of the values over the requested - axis. - - 'min' : Return the minimum of the values over the requested axis. - - 'pct_change' : Percentage change between the current and a prior - element. - - 'quantile' : Return values at the given quantile over requested - axis. - - 'sem' : Return unbiased standard error of the mean over requested - axis. - - 'skew' : Return unbiased skew over requested axis. - - 'std' : Return sample standard deviation over requested axis. - - 'var' : Return unbiased variance over requested axis. - - **kwargs - Options to pass to subjacent DataFrame method. - - Returns - ------- - :class:`matplotlib.axes.Axes` or numpy.ndarray of them - The ax used by the plot - - """ - kind = self._DEFAULT_KIND if kind is None else kind - - if kind.startswith("_"): - raise ValueError(f"invalid kind name '{kind}'") - - method = getattr(self, kind, None) - if not callable(method): - raise ValueError(f"Invalid kind name '{kind}'") - - return method(**kwargs) - - def __repr__(self): - """x.__repr__() <==> repr(x).""" - return f"{type(self).__name__}({self._dm})" - - def __getattr__(self, a): - """x.__getattr__(a) <==> x.a <==> getattr(x, "a").""" - if a not in self._DF_WHITELIST: - raise AttributeError(a) - return getattr(self._dm._data_df, a) - - def __dir__(self): - """x.__dir__() <==> dir(x).""" - return super().__dir__() + [ - e for e in dir(self._dm._data_df) if e in self._DF_WHITELIST - ] - - # ============================================================================= # _SLICER ARRAY # ============================================================================= diff --git a/skcriteria/core/stats.py b/skcriteria/core/stats.py new file mode 100644 index 0000000..9c0e981 --- /dev/null +++ b/skcriteria/core/stats.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Stats helper for the DecisionMatrix object.""" + + +# ============================================================================= +# STATS ACCESSOR +# ============================================================================= + + +class DecisionMatrixStatsAccessor: + """Calculate basic statistics of the decision matrix.""" + + _DF_WHITELIST = ( + "corr", + "cov", + "describe", + "kurtosis", + "mad", + "max", + "mean", + "median", + "min", + "pct_change", + "quantile", + "sem", + "skew", + "std", + "var", + ) + + _DEFAULT_KIND = "describe" + + def __init__(self, dm): + self._dm = dm + + def __call__(self, kind=None, **kwargs): + """Calculate basic statistics of the decision matrix. + + Parameters + ---------- + kind : str + The kind of statistic to produce: + + - 'corr' : Compute pairwise correlation of columns, excluding + NA/null values. + - 'cov' : Compute pairwise covariance of columns, excluding NA/null + values. + - 'describe' : Generate descriptive statistics. + - 'kurtosis' : Return unbiased kurtosis over requested axis. + - 'mad' : Return the mean absolute deviation of the values over the + requested axis. + - 'max' : Return the maximum of the values over the requested axis. + - 'mean' : Return the mean of the values over the requested axis. + - 'median' : Return the median of the values over the requested + axis. + - 'min' : Return the minimum of the values over the requested axis. + - 'pct_change' : Percentage change between the current and a prior + element. + - 'quantile' : Return values at the given quantile over requested + axis. + - 'sem' : Return unbiased standard error of the mean over requested + axis. + - 'skew' : Return unbiased skew over requested axis. + - 'std' : Return sample standard deviation over requested axis. + - 'var' : Return unbiased variance over requested axis. + + **kwargs + Options to pass to subjacent DataFrame method. + + Returns + ------- + :class:`matplotlib.axes.Axes` or numpy.ndarray of them + The ax used by the plot + + """ + kind = self._DEFAULT_KIND if kind is None else kind + + if kind.startswith("_"): + raise ValueError(f"invalid kind name '{kind}'") + + method = getattr(self, kind, None) + if not callable(method): + raise ValueError(f"Invalid kind name '{kind}'") + + return method(**kwargs) + + def __repr__(self): + """x.__repr__() <==> repr(x).""" + return f"{type(self).__name__}({self._dm})" + + def __getattr__(self, a): + """x.__getattr__(a) <==> x.a <==> getattr(x, "a").""" + if a not in self._DF_WHITELIST: + raise AttributeError(a) + return getattr(self._dm._data_df, a) + + def __dir__(self): + """x.__dir__() <==> dir(x).""" + return super().__dir__() + [ + e for e in dir(self._dm._data_df) if e in self._DF_WHITELIST + ] diff --git a/tests/core/test_stats.py b/tests/core/test_stats.py new file mode 100644 index 0000000..7b4e26e --- /dev/null +++ b/tests/core/test_stats.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.core.stats + +""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + +import numpy as np + +import pandas as pd + + +import pytest + +from skcriteria.core import data + + +# ============================================================================= +# STATS +# ============================================================================= + + +def test_DecisionMatrixStatsAccessor_default_kind(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + assert stats().equals(dm._data_df.describe()) + + +@pytest.mark.parametrize( + "kind", data.DecisionMatrixStatsAccessor._DF_WHITELIST +) +def test_DecisionMatrixStatsAccessor_df_whitelist_by_kind( + kind, decision_matrix +): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + expected = getattr(dm._data_df, kind)() + + stats = data.DecisionMatrixStatsAccessor(dm) + + result_call = stats(kind=kind) + + cmp = ( + lambda r, e: r.equals(e) + if isinstance(result_call, (pd.DataFrame, pd.Series)) + else np.equal + ) + + result_method = getattr(stats, kind)() + + assert cmp(result_call, expected) + assert cmp(result_method, expected) + + +def test_DecisionMatrixStatsAccessor_invalid_kind(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + with pytest.raises(ValueError): + stats("_dm") + + stats.foo = None + with pytest.raises(ValueError): + stats("foo") + + with pytest.raises(AttributeError): + stats.to_csv() + + +def test_DecisionMatrixStatsAccessor_repr(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + assert repr(stats) == f"DecisionMatrixStatsAccessor({repr(dm)})" + + +def test_DecisionMatrixStatsAccessor_dir(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + stats = data.DecisionMatrixStatsAccessor(dm) + + expected = set(data.DecisionMatrixStatsAccessor._DF_WHITELIST) + result = dir(stats) + + assert not expected.difference(result) From 4815f3bf102546e69a9590cb5e9dd42b15976eb3 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 10 Feb 2022 00:33:55 -0300 Subject: [PATCH 31/47] dominance manually tested need mooore --- docs/source/api/core/dominance.rst | 7 ++ skcriteria/core/data.py | 29 ++++- skcriteria/core/dominance.py | 190 +++++++++++++++++++++++++++++ skcriteria/core/stats.py | 4 +- skcriteria/utils/rank.py | 23 +++- tests/core/test_data.py | 106 +--------------- tests/core/test_dominance.py | 29 +++++ tests/core/test_plot.py | 2 +- 8 files changed, 276 insertions(+), 114 deletions(-) create mode 100644 docs/source/api/core/dominance.rst create mode 100644 skcriteria/core/dominance.py create mode 100644 tests/core/test_dominance.py diff --git a/docs/source/api/core/dominance.rst b/docs/source/api/core/dominance.rst new file mode 100644 index 0000000..09716ab --- /dev/null +++ b/docs/source/api/core/dominance.rst @@ -0,0 +1,7 @@ +``skcriteria.core.dominance`` module +==================================== + +.. automodule:: skcriteria.core.dominance + :members: + :undoc-members: + :show-inheritance: diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 635141f..7a45329 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -23,6 +23,7 @@ import enum import functools +import itertools as it from collections import abc import numpy as np @@ -33,9 +34,10 @@ import pyquery as pq +from .dominance import DecisionMatrixDominanceAccessor from .plot import DecisionMatrixPlotter from .stats import DecisionMatrixStatsAccessor -from ..utils import deprecated, doc_inherit +from ..utils import deprecated, doc_inherit, rank # ============================================================================= @@ -441,16 +443,41 @@ def dtypes(self): """Dtypes of the criteria.""" return self._data_df.dtypes.copy() + # ACCESSORS (YES, WE USE CACHED PROPERTIES IS THE EASIEST WAY) ============ + @property + @functools.lru_cache(maxsize=None) def plot(self): """Plot accessor.""" return DecisionMatrixPlotter(self) @property + @functools.lru_cache(maxsize=None) def stats(self): """Descriptive statistics accessor.""" return DecisionMatrixStatsAccessor(self) + @property + @functools.lru_cache(maxsize=None) + def dominance(self): + """Dominance information accessor.""" + + # Compute the dominance is an 0^2 algorithm, so lets use a cache + reverse = (self.objectives == Objective.MIN).to_numpy() + + dominance_cache, alts_numpy = {}, {} + + for a0, a1 in it.combinations(self.alternatives, 2): + for aname in (a0, a1): + if aname not in alts_numpy: + alts_numpy[aname] = self.alternatives[aname] + + dominance_cache[(a0, a1)] = rank.dominance( + alts_numpy[a0], alts_numpy[a1], reverse=reverse + ) + + return DecisionMatrixDominanceAccessor(self, dominance_cache) + # UTILITIES =============================================================== def copy(self, **kwargs): diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py new file mode 100644 index 0000000..0a5e75b --- /dev/null +++ b/skcriteria/core/dominance.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Dominance helper for the DecisionMatrix object.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= + +import functools +from collections import OrderedDict + +import numpy as np + +import pandas as pd + + +# ============================================================================= +# DOMINANCE ACCESSOR +# ============================================================================= + + +class DecisionMatrixDominanceAccessor: + """Calculate basic statistics of the decision matrix.""" + + _DEFAULT_KIND = "dominance" + + def __init__(self, dm, dominance_cache): + self._dm = dm + self._dominance_cache = dominance_cache + + def __call__(self, kind=None, **kwargs): + """Calculate basic statistics of the decision matrix. + + Parameters + ---------- + + + """ + kind = self._DEFAULT_KIND if kind is None else kind + + if kind.startswith("_"): + raise ValueError(f"invalid kind name '{kind}'") + + method = getattr(self, kind, None) + if not callable(method): + raise ValueError(f"Invalid kind name '{kind}'") + + return method(**kwargs) + + def __repr__(self): + """x.__repr__() <==> repr(x).""" + return f"{type(self).__name__}({self._dm})" + + def _create_frame(self, extract): + alternatives, domdict = self._dm.alternatives, self._dominance_cache + rows = [] + for a0 in alternatives: + row = OrderedDict() + for a1 in alternatives: + row[a1] = extract(a0, a1, domdict) + rows.append(row) + return pd.DataFrame(rows, index=alternatives) + + def bt(self): + def extract(a0, a1, domdict): + if a0 == a1: + return 0 + try: + return domdict[(a0, a1)].aDb + except KeyError: + return domdict[(a1, a0)].bDa + + return self._create_frame(extract) + + def eq(self): + alternatives_len = len(self._dm.alternatives) + + def extract(a0, a1, domdict): + if a0 == a1: + return alternatives_len + try: + return domdict[(a0, a1)].eq + except KeyError: + return domdict[(a1, a0)].eq + + return self._create_frame(extract) + + def dominance(self, strict=False): + def extract(a0, a1, domdict): + if a0 == a1: + return False + try: + info = domdict[(a0, a1)] + performance_a0, performance_a1 = info.aDb, info.bDa + except KeyError: + info = domdict[(a1, a0)] + performance_a1, performance_a0 = info.aDb, info.bDa + + if strict and info.eq: + return False + + return performance_a0 > 0 and performance_a1 == 0 + + return self._create_frame(extract) + + def resume(self, a0, a1): + domdict = self._dominance_cache + criteria = self._dm.criteria + + try: + info = domdict[(a0, a1)] + performance_a0, performance_a1 = info.aDb, info.bDa + where_aDb, where_bDa = info.aDb_where, info.bDa_where + except KeyError: + info = domdict[(a1, a0)] + performance_a1, performance_a0 = info.aDb, info.bDa + where_bDa, where_aDb = info.aDb_where, info.bDa_where + + alt_index = pd.MultiIndex.from_tuples( + [ + ("Alternatives", a0), + ("Alternatives", a1), + ("Equals", ""), + ] + ) + crit_index = pd.MultiIndex.from_product([["Criteria"], criteria]) + + df = pd.DataFrame( + [ + pd.Series(where_aDb, name=alt_index[0], index=crit_index), + pd.Series(where_bDa, name=alt_index[1], index=crit_index), + pd.Series(info.eq_where, name=alt_index[2], index=crit_index), + ] + ) + df.insert( + 3, + ("Performance"), + [performance_a0, performance_a1, info.eq], + ) + + return df + + def dominated(self, strict=False): + return self.dominance(strict=strict).any() + + @functools.lru_cache() + def dominators_of(self, a, strict=False): + + dominance_a = self.dominance(strict=strict)[a] + if ~dominance_a.any(): + return np.array([], dtype=str) + + dominators = dominance_a.index[dominance_a] + for dominator in dominators: + dominators_dominators = self.dominators_of( + dominator, strict=strict + ) + dominators = np.concatenate((dominators, dominators_dominators)) + return dominators + + def has_loops(self, strict=False): + # lets put the dominated alternatives last so our while loop will + # be shorter by extracting from the tail + alternatives = list(self.dominated(strict=strict).sort_values().index) + + try: + while alternatives: + # dame la ultima alternativa (al final quedan las dominadas) + alt = alternatives.pop() + + # ahora dame todas las alternatives las cuales dominan + dominators = self.dominators_of(alt, strict=strict) + + # las alternativas dominadoras ya pasaron por "dominated_by" + # por lo cual hay que sacarlas a todas de alternatives + alternatives = [a for a in alternatives if a not in dominators] + + except RecursionError: + return True + else: + return False diff --git a/skcriteria/core/stats.py b/skcriteria/core/stats.py index 9c0e981..f14d1c1 100644 --- a/skcriteria/core/stats.py +++ b/skcriteria/core/stats.py @@ -79,8 +79,8 @@ def __call__(self, kind=None, **kwargs): Returns ------- - :class:`matplotlib.axes.Axes` or numpy.ndarray of them - The ax used by the plot + object: array, float, int, frame or series + Statistic result. """ kind = self._DEFAULT_KIND if kind is None else kind diff --git a/skcriteria/utils/rank.py b/skcriteria/utils/rank.py index eed27da..85b200d 100644 --- a/skcriteria/utils/rank.py +++ b/skcriteria/utils/rank.py @@ -88,6 +88,9 @@ def dominance(array_a, array_b, reverse=False): reverse: bool (default=False) array_a[i] ≻ array_b[i] if array_a[i] > array_b[i] if reverse is False, otherwise array_a[i] ≻ array_b[i] if array_a[i] < array_b[i]. + Also revese can be an array of boolean of the same shape as + array_a and array_b to revert every item independently. + In other words, reverse assume the data is a minimization problem. Returns ------- @@ -110,14 +113,24 @@ def dominance(array_a, array_b, reverse=False): if np.shape(array_a) != np.shape(array_b): raise ValueError("array_a and array_b must be of the same shape") - domfunc = np.less if reverse else np.greater + if isinstance(reverse, bool): + reverse = np.full(np.shape(array_a), reverse) + elif np.shape(array_a) != np.shape(reverse): + raise ValueError( + "reverse must be a bool or an iterable of the same " + "shape than the arrays" + ) - array_a = np.asarray(array_a, dtype=int) - array_b = np.asarray(array_b, dtype=int) + array_a = np.asarray(array_a) + array_b = np.asarray(array_b) eq_where = array_a == array_b - aDb_where = domfunc(array_a, array_b) - bDa_where = domfunc(array_b, array_a) + aDb_where = np.where( + reverse, + array_a < array_b, + array_a > array_b, + ) + bDa_where = ~(aDb_where | eq_where) # a not dominates b and a != b return _Dominance( # resume diff --git a/tests/core/test_data.py b/tests/core/test_data.py index ed46108..52e9a07 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -9,7 +9,7 @@ # DOCS # ============================================================================= -"""test for skcriteria.data +"""test for skcriteria.core.data """ @@ -68,110 +68,6 @@ def test_objective_to_string(): assert data.Objective.MIN.to_string() == data.Objective._MIN_STR.value -# ============================================================================= -# STATS -# ============================================================================= - - -def test_DecisionMatrixStatsAccessor_default_kind(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - stats = data.DecisionMatrixStatsAccessor(dm) - - assert stats().equals(dm._data_df.describe()) - - -@pytest.mark.parametrize( - "kind", data.DecisionMatrixStatsAccessor._DF_WHITELIST -) -def test_DecisionMatrixStatsAccessor_df_whitelist_by_kind( - kind, decision_matrix -): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - expected = getattr(dm._data_df, kind)() - - stats = data.DecisionMatrixStatsAccessor(dm) - - result_call = stats(kind=kind) - - cmp = ( - lambda r, e: r.equals(e) - if isinstance(result_call, (pd.DataFrame, pd.Series)) - else np.equal - ) - - result_method = getattr(stats, kind)() - - assert cmp(result_call, expected) - assert cmp(result_method, expected) - - -def test_DecisionMatrixStatsAccessor_invalid_kind(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - stats = data.DecisionMatrixStatsAccessor(dm) - - with pytest.raises(ValueError): - stats("_dm") - - stats.foo = None - with pytest.raises(ValueError): - stats("foo") - - with pytest.raises(AttributeError): - stats.to_csv() - - -def test_DecisionMatrixStatsAccessor_repr(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - stats = data.DecisionMatrixStatsAccessor(dm) - - assert repr(stats) == f"DecisionMatrixStatsAccessor({repr(dm)})" - - -def test_DecisionMatrixStatsAccessor_dir(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - stats = data.DecisionMatrixStatsAccessor(dm) - - expected = set(data.DecisionMatrixStatsAccessor._DF_WHITELIST) - result = dir(stats) - - assert not expected.difference(result) - - # ============================================================================= # AC ACCESSORS # ============================================================================= diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py new file mode 100644 index 0000000..4c9ae38 --- /dev/null +++ b/tests/core/test_dominance.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.core.dominance + +""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= +# from unittest import mock + +# from matplotlib import pyplot as plt +# from matplotlib.testing.decorators import check_figures_equal + +# import pytest + +# import seaborn as sns + +# from skcriteria.core import plot diff --git a/tests/core/test_plot.py b/tests/core/test_plot.py index fe6651e..8ede8ad 100644 --- a/tests/core/test_plot.py +++ b/tests/core/test_plot.py @@ -29,7 +29,7 @@ from skcriteria.core import plot # ============================================================================= -# TEST IF __calll__ calls the correct method +# TEST IF __call__ calls the correct method # ============================================================================= From f71daeb4567f0c9800c4910bb70ef2ee65e95a97 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 10 Feb 2022 07:23:40 -0300 Subject: [PATCH 32/47] test new code of dominance in old modules --- tests/core/test_data.py | 49 ++++++++++++++++++++++++---------------- tests/utils/test_rank.py | 5 ++-- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/core/test_data.py b/tests/core/test_data.py index 52e9a07..654380a 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -26,7 +26,7 @@ import pytest -from skcriteria.core import data, plot +from skcriteria.core import data, dominance, plot, stats # ============================================================================= @@ -231,36 +231,45 @@ def test_DecisionMatrix_no_provide_cnames_and_anames(data_values): # ============================================================================= -def test_DecisionMatrix_plot(data_values): - mtx, objectives, weights, alternatives, criteria = data_values(seed=42) - - dm = data.mkdm( - matrix=mtx, - objectives=objectives, - weights=weights, - alternatives=alternatives, - criteria=criteria, +def test_DecisionMatrix_plot(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, ) assert isinstance(dm.plot, plot.DecisionMatrixPlotter) assert dm.plot._dm is dm -def test_DecisionMatrix_stats(data_values): - mtx, objectives, weights, alternatives, criteria = data_values(seed=42) - - dm = data.mkdm( - matrix=mtx, - objectives=objectives, - weights=weights, - alternatives=alternatives, - criteria=criteria, +def test_DecisionMatrix_stats(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, ) - assert isinstance(dm.stats, data.DecisionMatrixStatsAccessor) + assert isinstance(dm.stats, stats.DecisionMatrixStatsAccessor) assert dm.stats._dm is dm +def test_DecisionMatrix_dominance(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=10, + max_alternatives=10, + min_criteria=3, + max_criteria=3, + ) + + assert isinstance(dm.dominance, dominance.DecisionMatrixDominanceAccessor) + assert dm.dominance._dm is dm + + # ============================================================================= # DECISION MATRIX # ============================================================================= diff --git a/tests/utils/test_rank.py b/tests/utils/test_rank.py index ae0a473..0a25c46 100644 --- a/tests/utils/test_rank.py +++ b/tests/utils/test_rank.py @@ -86,6 +86,7 @@ def test_dominance_reverse(ra, rb): def test_dominance_fail(): - ra, rb = [1], [1, 2] with pytest.raises(ValueError): - rank.dominance(ra, rb) + rank.dominance([1], [1, 2]) + with pytest.raises(ValueError): + rank.dominance([3, 4], [1, 2], [True]) From f82f4d46c0784d36fb28bb205b31d9c7d3945f06 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 11 Feb 2022 22:08:29 -0300 Subject: [PATCH 33/47] dominance works and tested... starting refactoring --- skcriteria/core/dominance.py | 51 ++- tests/core/test_dominance.py | 668 ++++++++++++++++++++++++++++++++++- tests/core/test_plot.py | 60 ++-- 3 files changed, 714 insertions(+), 65 deletions(-) diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 0a5e75b..1266f65 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -58,7 +58,7 @@ def __call__(self, kind=None, **kwargs): def __repr__(self): """x.__repr__() <==> repr(x).""" - return f"{type(self).__name__}({self._dm})" + return f"{type(self).__name__}({self._dm!r})" def _create_frame(self, extract): alternatives, domdict = self._dm.alternatives, self._dominance_cache @@ -94,24 +94,6 @@ def extract(a0, a1, domdict): return self._create_frame(extract) - def dominance(self, strict=False): - def extract(a0, a1, domdict): - if a0 == a1: - return False - try: - info = domdict[(a0, a1)] - performance_a0, performance_a1 = info.aDb, info.bDa - except KeyError: - info = domdict[(a1, a0)] - performance_a1, performance_a0 = info.aDb, info.bDa - - if strict and info.eq: - return False - - return performance_a0 > 0 and performance_a1 == 0 - - return self._create_frame(extract) - def resume(self, a0, a1): domdict = self._dominance_cache criteria = self._dm.criteria @@ -141,18 +123,35 @@ def resume(self, a0, a1): pd.Series(info.eq_where, name=alt_index[2], index=crit_index), ] ) - df.insert( - 3, - ("Performance"), - [performance_a0, performance_a1, info.eq], + + df = df.assign( + Performance=[performance_a0, performance_a1, info.eq], ) return df + def dominance(self, strict=False): + def extract(a0, a1, domdict): + if a0 == a1: + return False + try: + info = domdict[(a0, a1)] + performance_a0, performance_a1 = info.aDb, info.bDa + except KeyError: + info = domdict[(a1, a0)] + performance_a1, performance_a0 = info.aDb, info.bDa + + if strict and info.eq: + return False + + return performance_a0 > 0 and performance_a1 == 0 + + return self._create_frame(extract) + def dominated(self, strict=False): return self.dominance(strict=strict).any() - @functools.lru_cache() + @functools.lru_cache(maxsize=None) def dominators_of(self, a, strict=False): dominance_a = self.dominance(strict=strict)[a] @@ -170,6 +169,7 @@ def dominators_of(self, a, strict=False): def has_loops(self, strict=False): # lets put the dominated alternatives last so our while loop will # be shorter by extracting from the tail + alternatives = list(self.dominated(strict=strict).sort_values().index) try: @@ -186,5 +186,4 @@ def has_loops(self, strict=False): except RecursionError: return True - else: - return False + return False diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py index 4c9ae38..53b681a 100644 --- a/tests/core/test_dominance.py +++ b/tests/core/test_dominance.py @@ -17,13 +17,669 @@ # ============================================================================= # IMPORTS # ============================================================================= -# from unittest import mock -# from matplotlib import pyplot as plt -# from matplotlib.testing.decorators import check_figures_equal +import inspect +from unittest import mock -# import pytest +import numpy as np -# import seaborn as sns +import pandas as pd -# from skcriteria.core import plot +import pytest + +from skcriteria.core import data, dominance +from skcriteria.utils import rank + +# ============================================================================= +# TEST IF __call__ calls the correct method +# ============================================================================= + + +def test_DecisionMatrixDominanceAccessor_call_invalid_kind(decision_matrix): + dm = decision_matrix( + seed=42, + min_alternatives=3, + max_alternatives=3, + min_criteria=3, + max_criteria=3, + ) + + dom = dominance.DecisionMatrixDominanceAccessor(dm, {}) + + with pytest.raises(ValueError): + dom("__call__") + + dom.zaraza = None # not callable + with pytest.raises(ValueError): + dom("zaraza") + + +@pytest.mark.parametrize( + "kind", + { + kind + for kind, kind_method in vars( + dominance.DecisionMatrixDominanceAccessor + ).items() + if not inspect.ismethod(kind_method) and not kind.startswith("_") + }, +) +def test_DecisionMatrixDominanceAccessor_call(decision_matrix, kind): + dm = decision_matrix( + seed=42, + min_alternatives=3, + max_alternatives=3, + min_criteria=3, + max_criteria=3, + ) + + dom = dominance.DecisionMatrixDominanceAccessor(dm, {}) + + method_name = ( + f"skcriteria.core.dominance.DecisionMatrixDominanceAccessor.{kind}" + ) + + with mock.patch(method_name) as plot_method: + dom(kind=kind) + + plot_method.assert_called_once() + + +# ============================================================================= +# BT +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_bt(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 40], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.DataFrame( + [ + [0, 1], + [1, 0], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + pd.testing.assert_frame_equal(dom.bt(), expected) + pd.testing.assert_frame_equal(dm.dominance.bt(), expected) + + +def test_DecisionMatrixDominanceAccessor_repr(): + dm = data.mkdm( + matrix=[ + [10, 70], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + cache = { + ("A0", "A1"): rank.dominance( + array_a=dm.alternatives["A0"].to_numpy(), + array_b=dm.alternatives["A1"].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + expected = ( + "DecisionMatrixDominanceAccessor( C0[▲ 1.0] C1[▼ 1.0]\n" + "A0 10 70\n" + "A1 20 70\n" + "[2 Alternatives x 2 Criteria])" + ) + + assert repr(dom) == expected + + +# ============================================================================= +# EQ +# ============================================================================= +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_eq(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 70], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.DataFrame( + [ + [2, 1], + [1, 2], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + pd.testing.assert_frame_equal(dom.eq(), expected) + pd.testing.assert_frame_equal(dm.dominance.eq(), expected) + + +# ============================================================================= +# RESUME +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_resume(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 70], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.DataFrame.from_dict( + { + ("Criteria", "C0"): { + ("Alternatives", "A0"): False, + ("Alternatives", "A1"): True, + ("Equals", ""): False, + }, + ("Criteria", "C1"): { + ("Alternatives", "A0"): False, + ("Alternatives", "A1"): False, + ("Equals", ""): True, + }, + ("Performance", ""): { + ("Alternatives", "A0"): 0, + ("Alternatives", "A1"): 1, + ("Equals", ""): 1, + }, + } + ) + + result = dom.resume("A0", "A1") + + pd.testing.assert_frame_equal(result, expected) + + +# ============================================================================= +# DOMINANCE +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominance(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 80], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.DataFrame( + [ + [False, False], + [True, False], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + pd.testing.assert_frame_equal(dom.dominance(), expected) + pd.testing.assert_frame_equal(dm.dominance.dominance(), expected) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominance_strict(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 80], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.DataFrame( + [ + [False, False], + [True, False], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + pd.testing.assert_frame_equal(dom.dominance(strict=True), expected) + pd.testing.assert_frame_equal( + dm.dominance.dominance(strict=True), expected + ) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominance_strict_false( + revert_key_cache, +): + dm = data.mkdm( + matrix=[ + [10, 80], + [10, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + strict_expected = pd.DataFrame( + [ + [False, False], + [False, False], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + pd.testing.assert_frame_equal(dom.dominance(strict=True), strict_expected) + pd.testing.assert_frame_equal( + dm.dominance.dominance(strict=True), strict_expected + ) + + not_strict_expected = pd.DataFrame( + [ + [False, False], + [True, False], + ], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + pd.testing.assert_frame_equal( + dom.dominance(strict=False), not_strict_expected + ) + pd.testing.assert_frame_equal( + dm.dominance.dominance(strict=False), not_strict_expected + ) + + +# ============================================================================= +# DOMINATED +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominated(revert_key_cache): + + dm = data.mkdm( + matrix=[ + [10, 80], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.Series([True, False], index=["A0", "A1"]) + + pd.testing.assert_series_equal(dom.dominated(), expected) + pd.testing.assert_series_equal(dm.dominance.dominated(), expected) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominated_strict(revert_key_cache): + dm = data.mkdm( + matrix=[ + [10, 80], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + expected = pd.Series([True, False], index=["A0", "A1"]) + + pd.testing.assert_series_equal(dom.dominated(strict=True), expected) + pd.testing.assert_series_equal( + dm.dominance.dominated(strict=True), expected + ) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominated_strict_false( + revert_key_cache, +): + dm = data.mkdm( + matrix=[ + [10, 80], + [10, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + strict_expected = pd.Series([False, False], index=["A0", "A1"]) + + pd.testing.assert_series_equal(dom.dominated(strict=True), strict_expected) + pd.testing.assert_series_equal( + dm.dominance.dominated(strict=True), strict_expected + ) + + not_strict_expected = pd.Series([True, False], index=["A0", "A1"]) + + pd.testing.assert_series_equal( + dom.dominated(strict=False), not_strict_expected + ) + pd.testing.assert_series_equal( + dm.dominance.dominated(strict=False), not_strict_expected + ) + + +# ============================================================================= +# DOMINATORS OF +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominators_of(revert_key_cache): + + dm = data.mkdm( + matrix=[ + [10, 80], + [20, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + assert np.all(dom.dominators_of("A0") == ["A1"]) + assert np.all(dm.dominance.dominators_of("A0") == ["A1"]) + + assert not len(dom.dominators_of("A1")) + assert not len(dm.dominance.dominators_of("A1")) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominators_of_strict( + revert_key_cache, +): + dm = data.mkdm( + matrix=[ + [20, 80], + [20, 90], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + assert not len(dom.dominators_of("A1", strict=True)) + assert not len(dm.dominance.dominators_of("A1", strict=True)) + + assert not len(dom.dominators_of("A0", strict=True)) + assert not len(dm.dominance.dominators_of("A0", strict=True)) + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_dominators_of_strict_false( + revert_key_cache, +): + dm = data.mkdm( + matrix=[ + [10, 80], + [10, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + assert np.all(dom.dominators_of("A0", strict=False) == ["A1"]) + assert not len(dom.dominators_of("A0", strict=True)) + + +# ============================================================================= +# HAS LOOPS +# ============================================================================= + + +@pytest.mark.parametrize( + "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format +) +def test_DecisionMatrixDominanceAccessor_has_loops_false( + revert_key_cache, +): + dm = data.mkdm( + matrix=[ + [10, 80], + [10, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") + + cache = { + (key0, key1): rank.dominance( + array_a=dm.alternatives[key0].to_numpy(), + array_b=dm.alternatives[key1].to_numpy(), + reverse=(dm.objectives == data.Objective.MIN).to_numpy(), + ) + } + + dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + + assert not dom.has_loops(strict=True) + assert not dom.has_loops(strict=False) + + +def test_DecisionMatrixDominanceAccessor_has_loops_true(): + """This test is complex so we relay on a hack""" + + dm = data.mkdm( + matrix=[ + [10, 80], + [10, 70], + ], + objectives=[max, min], + alternatives=["A0", "A1"], + criteria=["C0", "C1"], + ) + + fake_dominance = pd.DataFrame( + [[False, True], [True, False]], + index=["A0", "A1"], + columns=["A0", "A1"], + ) + + with mock.patch.object( + dm.dominance, "dominance", return_value=fake_dominance + ): + + assert dm.dominance.has_loops() diff --git a/tests/core/test_plot.py b/tests/core/test_plot.py index 8ede8ad..5233efc 100644 --- a/tests/core/test_plot.py +++ b/tests/core/test_plot.py @@ -17,6 +17,8 @@ # ============================================================================= # IMPORTS # ============================================================================= + +import inspect from unittest import mock from matplotlib import pyplot as plt @@ -33,7 +35,7 @@ # ============================================================================= -def test_plot_call_invalid_plot_kind(decision_matrix): +def test_DecisionMatrixPlotter_call_invalid_plot_kind(decision_matrix): dm = decision_matrix( seed=42, min_alternatives=3, @@ -54,23 +56,13 @@ def test_plot_call_invalid_plot_kind(decision_matrix): @pytest.mark.parametrize( "plot_kind", - [ - "heatmap", - "wheatmap", - "bar", - "wbar", - "barh", - "wbarh", - "hist", - "whist", - "box", - "wbox", - "kde", - "wkde", - "area", - ], + { + pkind + for pkind, pkind_method in vars(plot.DecisionMatrixPlotter).items() + if not inspect.ismethod(pkind_method) and not pkind.startswith("_") + }, ) -def test_plot_call_heatmap(decision_matrix, plot_kind): +def test_DecisionMatrixPlotter_call(decision_matrix, plot_kind): dm = decision_matrix( seed=42, min_alternatives=3, @@ -96,7 +88,7 @@ def test_plot_call_heatmap(decision_matrix, plot_kind): @pytest.mark.slow @check_figures_equal() -def test_plot_heatmap(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_heatmap(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -125,7 +117,7 @@ def test_plot_heatmap(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wheatmap(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wheatmap(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -155,7 +147,9 @@ def test_plot_wheatmap(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wheatmap_default_axis(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wheatmap_default_axis( + decision_matrix, fig_test, fig_ref +): dm = decision_matrix( seed=42, min_alternatives=3, @@ -194,7 +188,7 @@ def test_plot_wheatmap_default_axis(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_bar(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_bar(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -222,7 +216,7 @@ def test_plot_bar(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wbar(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wbar(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -256,7 +250,7 @@ def test_plot_wbar(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_barh(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_barh(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -284,7 +278,7 @@ def test_plot_barh(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wbarh(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wbarh(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -317,7 +311,7 @@ def test_plot_wbarh(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_hist(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_hist(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -344,7 +338,7 @@ def test_plot_hist(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_whist(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_whist(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -378,7 +372,7 @@ def test_plot_whist(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @pytest.mark.parametrize("orient", ["v", "h"]) @check_figures_equal() -def test_plot_box(decision_matrix, orient, fig_test, fig_ref): +def test_DecisionMatrixPlotter_box(decision_matrix, orient, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -410,7 +404,7 @@ def test_plot_box(decision_matrix, orient, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wbox(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wbox(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -436,7 +430,7 @@ def test_plot_wbox(decision_matrix, fig_test, fig_ref): # ============================================================================= @pytest.mark.slow @check_figures_equal() -def test_plot_kde(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_kde(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -463,7 +457,7 @@ def test_plot_kde(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wkde(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wkde(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -491,7 +485,7 @@ def test_plot_wkde(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_ogive(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_ogive(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -518,7 +512,7 @@ def test_plot_ogive(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_wogive(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_wogive(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, @@ -546,7 +540,7 @@ def test_plot_wogive(decision_matrix, fig_test, fig_ref): @pytest.mark.slow @check_figures_equal() -def test_plot_area(decision_matrix, fig_test, fig_ref): +def test_DecisionMatrixPlotter_area(decision_matrix, fig_test, fig_ref): dm = decision_matrix( seed=42, min_alternatives=3, From 55fd5d33ac90c2f92da994ca94b460586c39a241 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Fri, 11 Feb 2022 22:28:13 -0300 Subject: [PATCH 34/47] creation of the cache of dominance is managed inside the accessor --- skcriteria/core/data.py | 34 +++-- skcriteria/core/dominance.py | 19 ++- tests/core/test_dominance.py | 247 +++++------------------------------ 3 files changed, 65 insertions(+), 235 deletions(-) diff --git a/skcriteria/core/data.py b/skcriteria/core/data.py index 7a45329..adec9fd 100644 --- a/skcriteria/core/data.py +++ b/skcriteria/core/data.py @@ -23,7 +23,6 @@ import enum import functools -import itertools as it from collections import abc import numpy as np @@ -37,7 +36,7 @@ from .dominance import DecisionMatrixDominanceAccessor from .plot import DecisionMatrixPlotter from .stats import DecisionMatrixStatsAccessor -from ..utils import deprecated, doc_inherit, rank +from ..utils import deprecated, doc_inherit # ============================================================================= @@ -410,6 +409,20 @@ def objectives(self): name="Objectives", ) + @property + def minwhere(self): + """Mask with value True if the criterion is to be minimized.""" + mask = self.objectives == Objective.MIN + mask.name = "minwhere" + return mask + + @property + def maxwhere(self): + """Mask with value True if the criterion is to be maximized.""" + mask = self.objectives == Objective.MAX + mask.name = "maxwhere" + return mask + # READ ONLY PROPERTIES ==================================================== @property @@ -461,22 +474,7 @@ def stats(self): @functools.lru_cache(maxsize=None) def dominance(self): """Dominance information accessor.""" - - # Compute the dominance is an 0^2 algorithm, so lets use a cache - reverse = (self.objectives == Objective.MIN).to_numpy() - - dominance_cache, alts_numpy = {}, {} - - for a0, a1 in it.combinations(self.alternatives, 2): - for aname in (a0, a1): - if aname not in alts_numpy: - alts_numpy[aname] = self.alternatives[aname] - - dominance_cache[(a0, a1)] = rank.dominance( - alts_numpy[a0], alts_numpy[a1], reverse=reverse - ) - - return DecisionMatrixDominanceAccessor(self, dominance_cache) + return DecisionMatrixDominanceAccessor(self) # UTILITIES =============================================================== diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 1266f65..291c7cf 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -16,12 +16,14 @@ # ============================================================================= import functools +import itertools as it from collections import OrderedDict import numpy as np import pandas as pd +from ..utils import rank # ============================================================================= # DOMINANCE ACCESSOR @@ -33,8 +35,23 @@ class DecisionMatrixDominanceAccessor: _DEFAULT_KIND = "dominance" - def __init__(self, dm, dominance_cache): + def __init__(self, dm): self._dm = dm + + # Compute the dominance is an 0^2 algorithm, so lets use a cache + reverse = dm.minwhere + + dominance_cache, alts_numpy = {}, {} + + for a0, a1 in it.combinations(dm.alternatives, 2): + for aname in (a0, a1): + if aname not in alts_numpy: + alts_numpy[aname] = dm.alternatives[aname] + + dominance_cache[(a0, a1)] = rank.dominance( + alts_numpy[a0], alts_numpy[a1], reverse=reverse + ) + self._dominance_cache = dominance_cache def __call__(self, kind=None, **kwargs): diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py index 53b681a..62cc7c7 100644 --- a/tests/core/test_dominance.py +++ b/tests/core/test_dominance.py @@ -28,7 +28,6 @@ import pytest from skcriteria.core import data, dominance -from skcriteria.utils import rank # ============================================================================= # TEST IF __call__ calls the correct method @@ -44,7 +43,7 @@ def test_DecisionMatrixDominanceAccessor_call_invalid_kind(decision_matrix): max_criteria=3, ) - dom = dominance.DecisionMatrixDominanceAccessor(dm, {}) + dom = dominance.DecisionMatrixDominanceAccessor(dm) with pytest.raises(ValueError): dom("__call__") @@ -73,7 +72,7 @@ def test_DecisionMatrixDominanceAccessor_call(decision_matrix, kind): max_criteria=3, ) - dom = dominance.DecisionMatrixDominanceAccessor(dm, {}) + dom = dominance.DecisionMatrixDominanceAccessor(dm) method_name = ( f"skcriteria.core.dominance.DecisionMatrixDominanceAccessor.{kind}" @@ -90,10 +89,7 @@ def test_DecisionMatrixDominanceAccessor_call(decision_matrix, kind): # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_bt(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_bt(): dm = data.mkdm( matrix=[ [10, 40], @@ -104,17 +100,7 @@ def test_DecisionMatrixDominanceAccessor_bt(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.DataFrame( [ @@ -138,15 +124,8 @@ def test_DecisionMatrixDominanceAccessor_repr(): alternatives=["A0", "A1"], criteria=["C0", "C1"], ) - cache = { - ("A0", "A1"): rank.dominance( - array_a=dm.alternatives["A0"].to_numpy(), - array_b=dm.alternatives["A1"].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = ( "DecisionMatrixDominanceAccessor( C0[▲ 1.0] C1[▼ 1.0]\n" "A0 10 70\n" @@ -160,10 +139,9 @@ def test_DecisionMatrixDominanceAccessor_repr(): # ============================================================================= # EQ # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_eq(revert_key_cache): + + +def test_DecisionMatrixDominanceAccessor_eq(): dm = data.mkdm( matrix=[ [10, 70], @@ -174,17 +152,7 @@ def test_DecisionMatrixDominanceAccessor_eq(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.DataFrame( [ @@ -204,10 +172,7 @@ def test_DecisionMatrixDominanceAccessor_eq(revert_key_cache): # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_resume(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_resume(): dm = data.mkdm( matrix=[ [10, 70], @@ -218,17 +183,7 @@ def test_DecisionMatrixDominanceAccessor_resume(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.DataFrame.from_dict( { @@ -260,10 +215,7 @@ def test_DecisionMatrixDominanceAccessor_resume(revert_key_cache): # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominance(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_dominance(): dm = data.mkdm( matrix=[ [10, 80], @@ -274,17 +226,7 @@ def test_DecisionMatrixDominanceAccessor_dominance(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.DataFrame( [ @@ -299,10 +241,7 @@ def test_DecisionMatrixDominanceAccessor_dominance(revert_key_cache): pd.testing.assert_frame_equal(dm.dominance.dominance(), expected) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominance_strict(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_dominance_strict(): dm = data.mkdm( matrix=[ [10, 80], @@ -313,17 +252,7 @@ def test_DecisionMatrixDominanceAccessor_dominance_strict(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.DataFrame( [ @@ -340,12 +269,7 @@ def test_DecisionMatrixDominanceAccessor_dominance_strict(revert_key_cache): ) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominance_strict_false( - revert_key_cache, -): +def test_DecisionMatrixDominanceAccessor_dominance_strict_false(): dm = data.mkdm( matrix=[ [10, 80], @@ -356,17 +280,7 @@ def test_DecisionMatrixDominanceAccessor_dominance_strict_false( criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) strict_expected = pd.DataFrame( [ @@ -404,10 +318,7 @@ def test_DecisionMatrixDominanceAccessor_dominance_strict_false( # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominated(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_dominated(): dm = data.mkdm( matrix=[ @@ -419,17 +330,7 @@ def test_DecisionMatrixDominanceAccessor_dominated(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.Series([True, False], index=["A0", "A1"]) @@ -437,10 +338,7 @@ def test_DecisionMatrixDominanceAccessor_dominated(revert_key_cache): pd.testing.assert_series_equal(dm.dominance.dominated(), expected) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominated_strict(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_dominated_strict(): dm = data.mkdm( matrix=[ [10, 80], @@ -451,17 +349,7 @@ def test_DecisionMatrixDominanceAccessor_dominated_strict(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) expected = pd.Series([True, False], index=["A0", "A1"]) @@ -471,12 +359,7 @@ def test_DecisionMatrixDominanceAccessor_dominated_strict(revert_key_cache): ) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominated_strict_false( - revert_key_cache, -): +def test_DecisionMatrixDominanceAccessor_dominated_strict_false(): dm = data.mkdm( matrix=[ [10, 80], @@ -487,17 +370,7 @@ def test_DecisionMatrixDominanceAccessor_dominated_strict_false( criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) strict_expected = pd.Series([False, False], index=["A0", "A1"]) @@ -521,10 +394,7 @@ def test_DecisionMatrixDominanceAccessor_dominated_strict_false( # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominators_of(revert_key_cache): +def test_DecisionMatrixDominanceAccessor_dominators_of(): dm = data.mkdm( matrix=[ @@ -536,17 +406,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of(revert_key_cache): criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) assert np.all(dom.dominators_of("A0") == ["A1"]) assert np.all(dm.dominance.dominators_of("A0") == ["A1"]) @@ -555,12 +415,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of(revert_key_cache): assert not len(dm.dominance.dominators_of("A1")) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominators_of_strict( - revert_key_cache, -): +def test_DecisionMatrixDominanceAccessor_dominators_of_strict(): dm = data.mkdm( matrix=[ [20, 80], @@ -571,17 +426,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of_strict( criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) assert not len(dom.dominators_of("A1", strict=True)) assert not len(dm.dominance.dominators_of("A1", strict=True)) @@ -590,12 +435,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of_strict( assert not len(dm.dominance.dominators_of("A0", strict=True)) -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_dominators_of_strict_false( - revert_key_cache, -): +def test_DecisionMatrixDominanceAccessor_dominators_of_strict_false(): dm = data.mkdm( matrix=[ [10, 80], @@ -606,17 +446,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of_strict_false( criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) assert np.all(dom.dominators_of("A0", strict=False) == ["A1"]) assert not len(dom.dominators_of("A0", strict=True)) @@ -627,12 +457,7 @@ def test_DecisionMatrixDominanceAccessor_dominators_of_strict_false( # ============================================================================= -@pytest.mark.parametrize( - "revert_key_cache", [True, False], ids="Key cache Reverted: {}".format -) -def test_DecisionMatrixDominanceAccessor_has_loops_false( - revert_key_cache, -): +def test_DecisionMatrixDominanceAccessor_has_loops_false(): dm = data.mkdm( matrix=[ [10, 80], @@ -643,17 +468,7 @@ def test_DecisionMatrixDominanceAccessor_has_loops_false( criteria=["C0", "C1"], ) - key0, key1 = ("A1", "A0") if revert_key_cache else ("A0", "A1") - - cache = { - (key0, key1): rank.dominance( - array_a=dm.alternatives[key0].to_numpy(), - array_b=dm.alternatives[key1].to_numpy(), - reverse=(dm.objectives == data.Objective.MIN).to_numpy(), - ) - } - - dom = dominance.DecisionMatrixDominanceAccessor(dm, cache) + dom = dominance.DecisionMatrixDominanceAccessor(dm) assert not dom.has_loops(strict=True) assert not dom.has_loops(strict=False) From 2a2a5ae81535f34590d815c16ebb1250e477be4a Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sat, 12 Feb 2022 00:09:43 -0300 Subject: [PATCH 35/47] remove warnings --- skcriteria/core/dominance.py | 75 +++++++++++++++++++--------------- tests/core/test_data.py | 71 ++++++++++++++++++++++++-------- tests/utils/test_decorators.py | 6 +-- 3 files changed, 100 insertions(+), 52 deletions(-) diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 291c7cf..e8279d1 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -77,52 +77,61 @@ def __repr__(self): """x.__repr__() <==> repr(x).""" return f"{type(self).__name__}({self._dm!r})" + def _cache_read(self, a0, a1): + key = a0, a1 + cache = self._dominance_cache + entry, key_reverted = ( + (cache[key], False) if key in cache else (cache[key[::-1]], True) + ) + return entry, key_reverted + def _create_frame(self, extract): - alternatives, domdict = self._dm.alternatives, self._dominance_cache + alternatives = self._dm.alternatives rows = [] for a0 in alternatives: row = OrderedDict() for a1 in alternatives: - row[a1] = extract(a0, a1, domdict) + row[a1] = extract(a0, a1) rows.append(row) return pd.DataFrame(rows, index=alternatives) def bt(self): - def extract(a0, a1, domdict): + def extract(a0, a1): if a0 == a1: return 0 - try: - return domdict[(a0, a1)].aDb - except KeyError: - return domdict[(a1, a0)].bDa + centry, ckreverted = self._cache_read(a0, a1) + return centry.aDb if not ckreverted else centry.bDa return self._create_frame(extract) def eq(self): alternatives_len = len(self._dm.alternatives) - def extract(a0, a1, domdict): + def extract(a0, a1): if a0 == a1: return alternatives_len - try: - return domdict[(a0, a1)].eq - except KeyError: - return domdict[(a1, a0)].eq + centry, _ = self._cache_read(a0, a1) + return centry.eq return self._create_frame(extract) def resume(self, a0, a1): - domdict = self._dominance_cache - criteria = self._dm.criteria - try: - info = domdict[(a0, a1)] - performance_a0, performance_a1 = info.aDb, info.bDa - where_aDb, where_bDa = info.aDb_where, info.bDa_where - except KeyError: - info = domdict[(a1, a0)] - performance_a1, performance_a0 = info.aDb, info.bDa - where_bDa, where_aDb = info.aDb_where, info.bDa_where + # read the cache and extract the values + centry, ckreverted = self._cache_read(a0, a1) + performance_a0, performance_a1 = ( + (centry.aDb, centry.bDa) + if not ckreverted + else (centry.bDa, centry.aDb) + ) + where_aDb, where_bDa = ( + (centry.aDb_where, centry.bDa_where) + if not ckreverted + else (centry.bDa_where, centry.aDb_where) + ) + eq, eq_where = centry.eq, centry.eq_where + + criteria = self._dm.criteria alt_index = pd.MultiIndex.from_tuples( [ @@ -137,28 +146,28 @@ def resume(self, a0, a1): [ pd.Series(where_aDb, name=alt_index[0], index=crit_index), pd.Series(where_bDa, name=alt_index[1], index=crit_index), - pd.Series(info.eq_where, name=alt_index[2], index=crit_index), + pd.Series(eq_where, name=alt_index[2], index=crit_index), ] ) df = df.assign( - Performance=[performance_a0, performance_a1, info.eq], + Performance=[performance_a0, performance_a1, eq], ) return df def dominance(self, strict=False): - def extract(a0, a1, domdict): + def extract(a0, a1): if a0 == a1: return False - try: - info = domdict[(a0, a1)] - performance_a0, performance_a1 = info.aDb, info.bDa - except KeyError: - info = domdict[(a1, a0)] - performance_a1, performance_a0 = info.aDb, info.bDa - - if strict and info.eq: + centry, ckreverted = self._cache_read(a0, a1) + performance_a0, performance_a1 = ( + (centry.aDb, centry.bDa) + if not ckreverted + else (centry.bDa, centry.aDb) + ) + + if strict and centry.eq: return False return performance_a0 > 0 and performance_a1 == 0 diff --git a/tests/core/test_data.py b/tests/core/test_data.py index 654380a..0f68408 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -18,6 +18,8 @@ # IMPORTS # ============================================================================= +import warnings + import numpy as np import pandas as pd @@ -74,29 +76,31 @@ def test_objective_to_string(): def test__ACArray(decision_matrix): + with warnings.catch_warnings(): + + # see: https://stackoverflow.com/a/46721064 + warnings.simplefilter(action="ignore", category=FutureWarning) - content = ["a", "b", "c"] - mapping = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]} + content = ["a", "b", "c"] + mapping = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]} - arr = data._ACArray(content, mapping.__getitem__) + arr = data._ACArray(content, mapping.__getitem__) - assert arr["a"] == [1, 2, 3] - assert arr["b"] == [4, 5, 6] - assert arr["c"] == [7, 8, 9] + assert arr["a"] == [1, 2, 3] + assert arr["b"] == [4, 5, 6] + assert arr["c"] == [7, 8, 9] - assert arr[0] == "a" - assert arr[1] == "b" - assert arr[2] == "c" + assert list(arr) == content - assert dict(arr.items()) == mapping - assert list(arr.keys()) == list(arr) == content - assert sorted(list(arr.values())) == sorted(list(mapping.values())) + assert dict(arr.items()) == mapping + assert list(arr.keys()) == list(arr) == content + assert sorted(list(arr.values())) == sorted(list(mapping.values())) - with pytest.raises(AttributeError): - arr[0] = 1 + with pytest.raises(AttributeError): + arr[0] = 1 - with pytest.raises(IndexError): - arr["foo"] + with pytest.raises(IndexError): + arr["foo"] # ============================================================================= @@ -128,6 +132,13 @@ def test_DecisionMatrix_simple_creation(data_values): np.testing.assert_array_equal(dm.criteria, criteria) np.testing.assert_array_equal(dm.dtypes, [np.float64] * len(criteria)) + np.testing.assert_array_equal( + dm.minwhere, dm.objectives == data.Objective.MIN + ) + np.testing.assert_array_equal( + dm.maxwhere, dm.objectives == data.Objective.MAX + ) + def test_DecisionMatrix_no_provide_weights(data_values): mtx, objectives, _, alternatives, criteria = data_values(seed=42) @@ -152,6 +163,13 @@ def test_DecisionMatrix_no_provide_weights(data_values): np.testing.assert_array_equal(dm.alternatives, alternatives) np.testing.assert_array_equal(dm.criteria, criteria) + np.testing.assert_array_equal( + dm.minwhere, dm.objectives == data.Objective.MIN + ) + np.testing.assert_array_equal( + dm.maxwhere, dm.objectives == data.Objective.MAX + ) + def test_DecisionMatrix_no_provide_anames(data_values): @@ -177,6 +195,13 @@ def test_DecisionMatrix_no_provide_anames(data_values): np.testing.assert_array_equal(dm.alternatives, alternatives) np.testing.assert_array_equal(dm.criteria, criteria) + np.testing.assert_array_equal( + dm.minwhere, dm.objectives == data.Objective.MIN + ) + np.testing.assert_array_equal( + dm.maxwhere, dm.objectives == data.Objective.MAX + ) + def test_DecisionMatrix_no_provide_cnames(data_values): mtx, objectives, weights, alternatives, _ = data_values(seed=42) @@ -201,6 +226,13 @@ def test_DecisionMatrix_no_provide_cnames(data_values): np.testing.assert_array_equal(dm.alternatives, alternatives) np.testing.assert_array_equal(dm.criteria, criteria) + np.testing.assert_array_equal( + dm.minwhere, dm.objectives == data.Objective.MIN + ) + np.testing.assert_array_equal( + dm.maxwhere, dm.objectives == data.Objective.MAX + ) + def test_DecisionMatrix_no_provide_cnames_and_anames(data_values): mtx, objectives, weights, _, _ = data_values(seed=42) @@ -225,6 +257,13 @@ def test_DecisionMatrix_no_provide_cnames_and_anames(data_values): np.testing.assert_array_equal(dm.alternatives, alternatives) np.testing.assert_array_equal(dm.criteria, criteria) + np.testing.assert_array_equal( + dm.minwhere, dm.objectives == data.Objective.MIN + ) + np.testing.assert_array_equal( + dm.maxwhere, dm.objectives == data.Objective.MAX + ) + # ============================================================================= # PROPERTIES diff --git a/tests/utils/test_decorators.py b/tests/utils/test_decorators.py index 422781e..1fc3506 100644 --- a/tests/utils/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -19,6 +19,7 @@ # ============================================================================= import string +import warnings import numpy as np @@ -61,14 +62,13 @@ def func_c(): class A: # noqa pass - with pytest.warns(None) as warnings: + with warnings.catch_warnings(): + warnings.simplefilter("error") @decorators.doc_inherit(doc, warn_class=False) class A: # noqa pass - assert not warnings - def test_deprecated(): def func(): From c4e2a60f90d8de1e20aa8707f0f46e1273769162 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sat, 12 Feb 2022 00:38:48 -0300 Subject: [PATCH 36/47] dominance filter implemented --- skcriteria/preprocessing/filters.py | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index 2a4577d..e859c86 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -21,7 +21,7 @@ import numpy as np -from ..core import SKCTransformerABC +from ..core import DecisionMatrix, SKCTransformerABC from ..utils import doc_inherit # ============================================================================= @@ -604,3 +604,46 @@ class FilterNotIn(SKCSetFilterABC): def _set_filter(self, arr, cond): return np.isin(arr, cond, invert=True) + + +# ============================================================================= +# DOMINANCE +# ============================================================================= + + +class FilterNonDominated(SKCTransformerABC): + + _skcriteria_parameters = frozenset(["strict"]) + + def __init__(self, *, strict=False): + self._strict = bool(strict) + + @property + def strict(self): + """""" + return self._strict + + @doc_inherit(SKCTransformerABC._transform_data) + def _transform_data(self, matrix, alternatives, dominated_mask, **kwargs): + + filtered_matrix = matrix[~dominated_mask] + filtered_alternatives = alternatives[~dominated_mask] + + kwargs.update( + matrix=filtered_matrix, + alternatives=filtered_alternatives, + ) + return kwargs + + def transform(self, dm): + + data = dm.to_dict() + dominated_mask = dm.dominance.dominated(strict=self._strict).to_numpy() + + transformed_data = self._transform_data( + dominated_mask=dominated_mask, **data + ) + + transformed_dm = DecisionMatrix.from_mcda_data(**transformed_data) + + return transformed_dm From d1eeff1ecbdb56db3e3ae138e02a91b2a8112574 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sun, 13 Feb 2022 20:41:50 -0300 Subject: [PATCH 37/47] tested dominance filter --- tests/preprocessing/test_filters.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/preprocessing/test_filters.py b/tests/preprocessing/test_filters.py index 40b657d..75024a2 100644 --- a/tests/preprocessing/test_filters.py +++ b/tests/preprocessing/test_filters.py @@ -539,3 +539,81 @@ def test_FilterNotIn(): result = tfm.transform(dm) assert result.equals(expected) + + +# ============================================================================= +# DOMINANCE +# ============================================================================= + + +def test_FilterNonDominated(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterNonDominated(strict=False) + + result = tfm.transform(dm) + + assert result.equals(expected) + + +def test_FilterNonDominated_strict(): + + dm = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + expected = skc.mkdm( + matrix=[ + [7, 5, 35], + [5, 4, 26], + [5, 6, 28], + [1, 7, 30], + [5, 8, 30], + ], + objectives=[max, max, min], + weights=[2, 4, 1], + alternatives=["PE", "JN", "AA", "MM", "FN"], + criteria=["ROE", "CAP", "RI"], + ) + + tfm = filters.FilterNonDominated(strict=True) + + result = tfm.transform(dm) + + assert result.equals(expected) From 8a59649488cc383b307383a7b2adb09b95aa8b58 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Wed, 16 Feb 2022 19:43:21 -0300 Subject: [PATCH 38/47] generalized accessor api --- docs/source/api/utils/accabc.rst | 7 +++ skcriteria/core/dominance.py | 34 ++++----------- skcriteria/core/plot.py | 75 +++++++++++++++----------------- skcriteria/core/stats.py | 68 +++++++++++++---------------- skcriteria/utils/__init__.py | 11 ++++- skcriteria/utils/accabc.py | 38 ++++++++++++++++ tests/core/test_dominance.py | 22 ---------- tests/core/test_plot.py | 51 ---------------------- tests/core/test_stats.py | 14 ------ tests/utils/test_accabc.py | 73 +++++++++++++++++++++++++++++++ 10 files changed, 202 insertions(+), 191 deletions(-) create mode 100644 docs/source/api/utils/accabc.rst create mode 100644 skcriteria/utils/accabc.py create mode 100644 tests/utils/test_accabc.py diff --git a/docs/source/api/utils/accabc.rst b/docs/source/api/utils/accabc.rst new file mode 100644 index 0000000..8750b0c --- /dev/null +++ b/docs/source/api/utils/accabc.rst @@ -0,0 +1,7 @@ +``skcriteria.core.accabc`` module +================================= + +.. automodule:: skcriteria.core.accabc + :members: + :undoc-members: + :show-inheritance: diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index e8279d1..f4e04d1 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -23,14 +23,14 @@ import pandas as pd -from ..utils import rank +from ..utils import AccessorABC, rank # ============================================================================= # DOMINANCE ACCESSOR # ============================================================================= -class DecisionMatrixDominanceAccessor: +class DecisionMatrixDominanceAccessor(AccessorABC): """Calculate basic statistics of the decision matrix.""" _DEFAULT_KIND = "dominance" @@ -38,7 +38,12 @@ class DecisionMatrixDominanceAccessor: def __init__(self, dm): self._dm = dm + @property + @functools.lru_cache(maxsize=None) + def _dominance_cache(self): # Compute the dominance is an 0^2 algorithm, so lets use a cache + dm = self._dm + reverse = dm.minwhere dominance_cache, alts_numpy = {}, {} @@ -52,30 +57,7 @@ def __init__(self, dm): alts_numpy[a0], alts_numpy[a1], reverse=reverse ) - self._dominance_cache = dominance_cache - - def __call__(self, kind=None, **kwargs): - """Calculate basic statistics of the decision matrix. - - Parameters - ---------- - - - """ - kind = self._DEFAULT_KIND if kind is None else kind - - if kind.startswith("_"): - raise ValueError(f"invalid kind name '{kind}'") - - method = getattr(self, kind, None) - if not callable(method): - raise ValueError(f"Invalid kind name '{kind}'") - - return method(**kwargs) - - def __repr__(self): - """x.__repr__() <==> repr(x).""" - return f"{type(self).__name__}({self._dm!r})" + return dominance_cache def _cache_read(self, a0, a1): key = a0, a1 diff --git a/skcriteria/core/plot.py b/skcriteria/core/plot.py index 26ba167..c691d1d 100644 --- a/skcriteria/core/plot.py +++ b/skcriteria/core/plot.py @@ -19,56 +19,53 @@ import seaborn as sns +from skcriteria.utils import AccessorABC + # ============================================================================= # PLOTTER OBJECT # ============================================================================= -class DecisionMatrixPlotter: - """Make plots of DecisionMatrix.""" +class DecisionMatrixPlotter(AccessorABC): + """Make plots of DecisionMatrix. - def __init__(self, dm): - self._dm = dm + Make plots of a decision matrix. - # INTERNAL ================================================================ + Parameters + ---------- + plot_kind : str + The kind of plot to produce: + - 'heatmap' : criteria heat-map (default). + - 'wheatmap' : weights heat-map. + - 'bar' : criteria vertical bar plot. + - 'wbar' : weights vertical bar plot. + - 'barh' : criteria horizontal bar plot. + - 'wbarh' : weights horizontal bar plot. + - 'hist' : criteria histogram. + - 'whist' : weights histogram. + - 'box' : criteria boxplot. + - 'wbox' : weights boxplot. + - 'kde' : criteria Kernel Density Estimation plot. + - 'wkde' : weights Kernel Density Estimation plot. + - 'ogive' : criteria empirical cumulative distribution plot. + - 'wogive' : weights empirical cumulative distribution plot. + - 'area' : criteria area plot. - def __call__(self, plot_kind="heatmap", **kwargs): - """Make plots of a decision matrix. + **kwargs + Options to pass to subjacent plotting method. - Parameters - ---------- - plot_kind : str - The kind of plot to produce: - - 'heatmap' : criteria heat-map (default). - - 'wheatmap' : weights heat-map. - - 'bar' : criteria vertical bar plot. - - 'wbar' : weights vertical bar plot. - - 'barh' : criteria horizontal bar plot. - - 'wbarh' : weights horizontal bar plot. - - 'hist' : criteria histogram. - - 'whist' : weights histogram. - - 'box' : criteria boxplot. - - 'wbox' : weights boxplot. - - 'kde' : criteria Kernel Density Estimation plot. - - 'wkde' : weights Kernel Density Estimation plot. - - 'ogive' : criteria empirical cumulative distribution plot. - - 'wogive' : weights empirical cumulative distribution plot. - - 'area' : criteria area plot. + Returns + ------- + :class:`matplotlib.axes.Axes` or numpy.ndarray of them + The ax used by the plot - **kwargs - Options to pass to subjacent plotting method. - Returns - ------- - :class:`matplotlib.axes.Axes` or numpy.ndarray of them - The ax used by the plot - """ - if plot_kind.startswith("_"): - raise ValueError(f"invalid plot_kind name '{plot_kind}'") - method = getattr(self, plot_kind, None) - if not callable(method): - raise ValueError(f"invalid plot_kind name '{plot_kind}'") - return method(**kwargs) + """ + + _DEFAULT_KIND = "heatmap" + + def __init__(self, dm): + self._dm = dm # PRIVATE ================================================================= # This method are used "a lot" inside all the different plots, so we can diff --git a/skcriteria/core/stats.py b/skcriteria/core/stats.py index f14d1c1..4506836 100644 --- a/skcriteria/core/stats.py +++ b/skcriteria/core/stats.py @@ -13,38 +13,20 @@ # ============================================================================= -# STATS ACCESSOR -# ============================================================================= - +# IMPORTS +# =============================================================================k -class DecisionMatrixStatsAccessor: - """Calculate basic statistics of the decision matrix.""" +from skcriteria.utils import AccessorABC - _DF_WHITELIST = ( - "corr", - "cov", - "describe", - "kurtosis", - "mad", - "max", - "mean", - "median", - "min", - "pct_change", - "quantile", - "sem", - "skew", - "std", - "var", - ) +# ============================================================================= +# STATS ACCESSOR +# ============================================================================= - _DEFAULT_KIND = "describe" - def __init__(self, dm): - self._dm = dm +class DecisionMatrixStatsAccessor(AccessorABC): + """Calculate basic statistics of the decision matrix. - def __call__(self, kind=None, **kwargs): - """Calculate basic statistics of the decision matrix. + Calculate basic statistics of the decision matrix. Parameters ---------- @@ -82,21 +64,31 @@ def __call__(self, kind=None, **kwargs): object: array, float, int, frame or series Statistic result. - """ - kind = self._DEFAULT_KIND if kind is None else kind - if kind.startswith("_"): - raise ValueError(f"invalid kind name '{kind}'") + """ - method = getattr(self, kind, None) - if not callable(method): - raise ValueError(f"Invalid kind name '{kind}'") + _DF_WHITELIST = ( + "corr", + "cov", + "describe", + "kurtosis", + "mad", + "max", + "mean", + "median", + "min", + "pct_change", + "quantile", + "sem", + "skew", + "std", + "var", + ) - return method(**kwargs) + _DEFAULT_KIND = "describe" - def __repr__(self): - """x.__repr__() <==> repr(x).""" - return f"{type(self).__name__}({self._dm})" + def __init__(self, dm): + self._dm = dm def __getattr__(self, a): """x.__getattr__(a) <==> x.a <==> getattr(x, "a").""" diff --git a/skcriteria/utils/__init__.py b/skcriteria/utils/__init__.py index a946591..5d955a9 100644 --- a/skcriteria/utils/__init__.py +++ b/skcriteria/utils/__init__.py @@ -16,6 +16,7 @@ # ============================================================================= from . import lp, rank +from .accabc import AccessorABC from .bunch import Bunch from .decorators import deprecated, doc_inherit @@ -23,4 +24,12 @@ # ALL # ============================================================================= -__all__ = ["doc_inherit", "deprecated", "rank", "Bunch", "lp", "dominance"] +__all__ = [ + "AccessorABC", + "doc_inherit", + "deprecated", + "rank", + "Bunch", + "lp", + "dominance", +] diff --git a/skcriteria/utils/accabc.py b/skcriteria/utils/accabc.py new file mode 100644 index 0000000..9262b84 --- /dev/null +++ b/skcriteria/utils/accabc.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""Accessor base class.""" + +# ============================================================================= +# ACESSOR ABC +# ============================================================================= + + +class AccessorABC: + + _DEFAULT_KIND = None + + def __init_subclass__(cls) -> None: + if cls._DEFAULT_KIND is None: + raise TypeError(f"{cls!r} must define a _DEFAULT_KIND") + + def __call__(self, kind=None, **kwargs): + """""" + kind = self._DEFAULT_KIND if kind is None else kind + + if kind.startswith("_"): + raise ValueError(f"invalid kind name '{kind}'") + + method = getattr(self, kind, None) + if not callable(method): + raise ValueError(f"Invalid kind name '{kind}'") + + return method(**kwargs) diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py index 62cc7c7..5d5946f 100644 --- a/tests/core/test_dominance.py +++ b/tests/core/test_dominance.py @@ -114,28 +114,6 @@ def test_DecisionMatrixDominanceAccessor_bt(): pd.testing.assert_frame_equal(dm.dominance.bt(), expected) -def test_DecisionMatrixDominanceAccessor_repr(): - dm = data.mkdm( - matrix=[ - [10, 70], - [20, 70], - ], - objectives=[max, min], - alternatives=["A0", "A1"], - criteria=["C0", "C1"], - ) - - dom = dominance.DecisionMatrixDominanceAccessor(dm) - expected = ( - "DecisionMatrixDominanceAccessor( C0[▲ 1.0] C1[▼ 1.0]\n" - "A0 10 70\n" - "A1 20 70\n" - "[2 Alternatives x 2 Criteria])" - ) - - assert repr(dom) == expected - - # ============================================================================= # EQ # ============================================================================= diff --git a/tests/core/test_plot.py b/tests/core/test_plot.py index 5233efc..3aef4a3 100644 --- a/tests/core/test_plot.py +++ b/tests/core/test_plot.py @@ -18,7 +18,6 @@ # IMPORTS # ============================================================================= -import inspect from unittest import mock from matplotlib import pyplot as plt @@ -30,56 +29,6 @@ from skcriteria.core import plot -# ============================================================================= -# TEST IF __call__ calls the correct method -# ============================================================================= - - -def test_DecisionMatrixPlotter_call_invalid_plot_kind(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=3, - max_alternatives=3, - min_criteria=3, - max_criteria=3, - ) - - plotter = plot.DecisionMatrixPlotter(dm=dm) - - with pytest.raises(ValueError): - plotter("__call__") - - plotter.zaraza = None # not callable - with pytest.raises(ValueError): - plotter("zaraza") - - -@pytest.mark.parametrize( - "plot_kind", - { - pkind - for pkind, pkind_method in vars(plot.DecisionMatrixPlotter).items() - if not inspect.ismethod(pkind_method) and not pkind.startswith("_") - }, -) -def test_DecisionMatrixPlotter_call(decision_matrix, plot_kind): - dm = decision_matrix( - seed=42, - min_alternatives=3, - max_alternatives=3, - min_criteria=3, - max_criteria=3, - ) - - plotter = plot.DecisionMatrixPlotter(dm=dm) - - method_name = f"skcriteria.core.plot.DecisionMatrixPlotter.{plot_kind}" - - with mock.patch(method_name) as plot_method: - plotter(plot_kind=plot_kind) - - plot_method.assert_called_once() - # ============================================================================= # HEATMAP diff --git a/tests/core/test_stats.py b/tests/core/test_stats.py index 7b4e26e..69941dd 100644 --- a/tests/core/test_stats.py +++ b/tests/core/test_stats.py @@ -101,20 +101,6 @@ def test_DecisionMatrixStatsAccessor_invalid_kind(decision_matrix): stats.to_csv() -def test_DecisionMatrixStatsAccessor_repr(decision_matrix): - dm = decision_matrix( - seed=42, - min_alternatives=10, - max_alternatives=10, - min_criteria=3, - max_criteria=3, - ) - - stats = data.DecisionMatrixStatsAccessor(dm) - - assert repr(stats) == f"DecisionMatrixStatsAccessor({repr(dm)})" - - def test_DecisionMatrixStatsAccessor_dir(decision_matrix): dm = decision_matrix( seed=42, diff --git a/tests/utils/test_accabc.py b/tests/utils/test_accabc.py new file mode 100644 index 0000000..4c21ec6 --- /dev/null +++ b/tests/utils/test_accabc.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised)) +# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia +# Copyright (c) 2022, QuatroPe +# All rights reserved. + +# ============================================================================= +# DOCS +# ============================================================================= + +"""test for skcriteria.utils.decorator + +""" + + +# ============================================================================= +# IMPORTS +# ============================================================================= + + +import numpy as np + +import pytest + +from skcriteria.utils import AccessorABC + + +# ============================================================================= +# TEST CLASSES +# ============================================================================= + + +def test_AccessorABC(): + class FooAccessor(AccessorABC): + + _DEFAULT_KIND = "zaraza" + + def __init__(self, v): + self._v = v + + def zaraza(self): + return self._v + + acc = FooAccessor(np.random.random()) + assert acc("zaraza") == acc.zaraza() == acc() + + +def test_AccessorABC_no__DEFAULT_KIND(): + with pytest.raises(TypeError): + + class FooAccessor(AccessorABC): + pass + + +def test_AccessorABC_invalid_kind(): + class FooAccessor(AccessorABC): + + _DEFAULT_KIND = "zaraza" + + def __init__(self): + self.dont_work = None + + def _zaraza(self): + pass + + acc = FooAccessor() + + with pytest.raises(ValueError): + acc("_zaraza") + + with pytest.raises(ValueError): + acc("dont_work") From ee53059dc0e7ed75bf29a1d644aa6f02c4309a69 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 00:58:47 -0300 Subject: [PATCH 39/47] docs! --- docs/source/api/utils/accabc.rst | 6 +- skcriteria/core/dominance.py | 113 ++++++++++++++++++++++++------- skcriteria/core/plot.py | 50 +++++--------- skcriteria/core/stats.py | 65 +++++++----------- skcriteria/utils/accabc.py | 39 +++++++++-- tests/utils/test_accabc.py | 9 ++- 6 files changed, 172 insertions(+), 110 deletions(-) diff --git a/docs/source/api/utils/accabc.rst b/docs/source/api/utils/accabc.rst index 8750b0c..b742226 100644 --- a/docs/source/api/utils/accabc.rst +++ b/docs/source/api/utils/accabc.rst @@ -1,7 +1,7 @@ -``skcriteria.core.accabc`` module -================================= +``skcriteria.utils.accabc`` module +================================== -.. automodule:: skcriteria.core.accabc +.. automodule:: skcriteria.utils.accabc :members: :undoc-members: :show-inheritance: diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index f4e04d1..6510699 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -33,7 +33,7 @@ class DecisionMatrixDominanceAccessor(AccessorABC): """Calculate basic statistics of the decision matrix.""" - _DEFAULT_KIND = "dominance" + _default_kind = "dominance" def __init__(self, dm): self._dm = dm @@ -41,7 +41,11 @@ def __init__(self, dm): @property @functools.lru_cache(maxsize=None) def _dominance_cache(self): - # Compute the dominance is an 0^2 algorithm, so lets use a cache + """Cache of dominance. + + Compute the dominance is an O(n_C_2) algorithm, so lets use a cache. + + """ dm = self._dm reverse = dm.minwhere @@ -60,6 +64,14 @@ def _dominance_cache(self): return dominance_cache def _cache_read(self, a0, a1): + """Return the entry of the cache. + + The input returned is the one that relates the alternatives a0 and a1. + Since the cache can store the entry with the key (a0, a1) or (a1, a0), + a second value is returned that is True if it was necessary to invert + the alternatives. + + """ key = a0, a1 cache = self._dominance_cache entry, key_reverted = ( @@ -67,35 +79,102 @@ def _cache_read(self, a0, a1): ) return entry, key_reverted - def _create_frame(self, extract): + # FRAME ALT VS ALT ======================================================== + + def _create_frame(self, compute_cell): + """Create a data frame comparing two alternatives. + + The value of each cell is calculated with the "compute_cell" + function. + + """ alternatives = self._dm.alternatives rows = [] for a0 in alternatives: row = OrderedDict() for a1 in alternatives: - row[a1] = extract(a0, a1) + row[a1] = compute_cell(a0, a1) rows.append(row) return pd.DataFrame(rows, index=alternatives) def bt(self): - def extract(a0, a1): + """Compare on how many criteria one alternative is better than another. + + *bt* = better-than. + + Returns + ------- + pandas.DataFrame: + Where the value of each cell identifies on how many criteria the + row alternative is better than the column alternative. + + """ + def compute_cell(a0, a1): if a0 == a1: return 0 centry, ckreverted = self._cache_read(a0, a1) return centry.aDb if not ckreverted else centry.bDa - return self._create_frame(extract) + return self._create_frame(compute_cell) def eq(self): + """Compare on how many criteria two alternatives are equal. + + Returns + ------- + pandas.DataFrame: + Where the value of each cell identifies how many criteria the row + and column alternatives are equal. + + """ alternatives_len = len(self._dm.alternatives) - def extract(a0, a1): + def compute_cell(a0, a1): if a0 == a1: return alternatives_len centry, _ = self._cache_read(a0, a1) return centry.eq - return self._create_frame(extract) + return self._create_frame(compute_cell) + + def dominance(self, strict=False): + """Compare if one alternative dominates or strictly dominates another \ + alternative. + + In order to evaluate the dominance of an alternative *a0* over an + alternative *a1*, the algorithm evaluates that *a0* is better in at + least one criterion and that *a1* is not better in any criterion than + *a0*. In the case that ``strict = True`` it also evaluates that there + are no equal criteria. + + Parameters + ---------- + strict: bool, default ``False`` + If True, strict dominance is evaluated. + + Returns + ------- + pandas.DataFrame: + Where the value of each cell is True if the row alternative + dominates the column alternative. + + """ + def compute_cell(a0, a1): + if a0 == a1: + return False + centry, ckreverted = self._cache_read(a0, a1) + performance_a0, performance_a1 = ( + (centry.aDb, centry.bDa) + if not ckreverted + else (centry.bDa, centry.aDb) + ) + + if strict and centry.eq: + return False + + return performance_a0 > 0 and performance_a1 == 0 + + return self._create_frame(compute_cell) def resume(self, a0, a1): @@ -138,24 +217,6 @@ def resume(self, a0, a1): return df - def dominance(self, strict=False): - def extract(a0, a1): - if a0 == a1: - return False - centry, ckreverted = self._cache_read(a0, a1) - performance_a0, performance_a1 = ( - (centry.aDb, centry.bDa) - if not ckreverted - else (centry.bDa, centry.aDb) - ) - - if strict and centry.eq: - return False - - return performance_a0 > 0 and performance_a1 == 0 - - return self._create_frame(extract) - def dominated(self, strict=False): return self.dominance(strict=strict).any() diff --git a/skcriteria/core/plot.py b/skcriteria/core/plot.py index c691d1d..0ccb8b1 100644 --- a/skcriteria/core/plot.py +++ b/skcriteria/core/plot.py @@ -28,41 +28,27 @@ class DecisionMatrixPlotter(AccessorABC): """Make plots of DecisionMatrix. - Make plots of a decision matrix. - - Parameters - ---------- - plot_kind : str - The kind of plot to produce: - - 'heatmap' : criteria heat-map (default). - - 'wheatmap' : weights heat-map. - - 'bar' : criteria vertical bar plot. - - 'wbar' : weights vertical bar plot. - - 'barh' : criteria horizontal bar plot. - - 'wbarh' : weights horizontal bar plot. - - 'hist' : criteria histogram. - - 'whist' : weights histogram. - - 'box' : criteria boxplot. - - 'wbox' : weights boxplot. - - 'kde' : criteria Kernel Density Estimation plot. - - 'wkde' : weights Kernel Density Estimation plot. - - 'ogive' : criteria empirical cumulative distribution plot. - - 'wogive' : weights empirical cumulative distribution plot. - - 'area' : criteria area plot. - - **kwargs - Options to pass to subjacent plotting method. - - Returns - ------- - :class:`matplotlib.axes.Axes` or numpy.ndarray of them - The ax used by the plot - - + Kind of plot to produce: + + - 'heatmap' : criteria heat-map (default). + - 'wheatmap' : weights heat-map. + - 'bar' : criteria vertical bar plot. + - 'wbar' : weights vertical bar plot. + - 'barh' : criteria horizontal bar plot. + - 'wbarh' : weights horizontal bar plot. + - 'hist' : criteria histogram. + - 'whist' : weights histogram. + - 'box' : criteria boxplot. + - 'wbox' : weights boxplot. + - 'kde' : criteria Kernel Density Estimation plot. + - 'wkde' : weights Kernel Density Estimation plot. + - 'ogive' : criteria empirical cumulative distribution plot. + - 'wogive' : weights empirical cumulative distribution plot. + - 'area' : criteria area plot. """ - _DEFAULT_KIND = "heatmap" + _default_kind = "heatmap" def __init__(self, dm): self._dm = dm diff --git a/skcriteria/core/stats.py b/skcriteria/core/stats.py index 4506836..620ddc1 100644 --- a/skcriteria/core/stats.py +++ b/skcriteria/core/stats.py @@ -26,47 +26,34 @@ class DecisionMatrixStatsAccessor(AccessorABC): """Calculate basic statistics of the decision matrix. - Calculate basic statistics of the decision matrix. - - Parameters - ---------- - kind : str - The kind of statistic to produce: - - - 'corr' : Compute pairwise correlation of columns, excluding - NA/null values. - - 'cov' : Compute pairwise covariance of columns, excluding NA/null - values. - - 'describe' : Generate descriptive statistics. - - 'kurtosis' : Return unbiased kurtosis over requested axis. - - 'mad' : Return the mean absolute deviation of the values over the - requested axis. - - 'max' : Return the maximum of the values over the requested axis. - - 'mean' : Return the mean of the values over the requested axis. - - 'median' : Return the median of the values over the requested - axis. - - 'min' : Return the minimum of the values over the requested axis. - - 'pct_change' : Percentage change between the current and a prior - element. - - 'quantile' : Return values at the given quantile over requested - axis. - - 'sem' : Return unbiased standard error of the mean over requested - axis. - - 'skew' : Return unbiased skew over requested axis. - - 'std' : Return sample standard deviation over requested axis. - - 'var' : Return unbiased variance over requested axis. - - **kwargs - Options to pass to subjacent DataFrame method. - - Returns - ------- - object: array, float, int, frame or series - Statistic result. - + Kind of statistic to produce: + + - 'corr' : Compute pairwise correlation of columns, excluding + NA/null values. + - 'cov' : Compute pairwise covariance of columns, excluding NA/null + values. + - 'describe' : Generate descriptive statistics. + - 'kurtosis' : Return unbiased kurtosis over requested axis. + - 'mad' : Return the mean absolute deviation of the values over the + requested axis. + - 'max' : Return the maximum of the values over the requested axis. + - 'mean' : Return the mean of the values over the requested axis. + - 'median' : Return the median of the values over the requested + axis. + - 'min' : Return the minimum of the values over the requested axis. + - 'pct_change' : Percentage change between the current and a prior + element. + - 'quantile' : Return values at the given quantile over requested + axis. + - 'sem' : Return unbiased standard error of the mean over requested + axis. + - 'skew' : Return unbiased skew over requested axis. + - 'std' : Return sample standard deviation over requested axis. + - 'var' : Return unbiased variance over requested axis. """ + # The list of methods that can be accessed of the subjacent dataframe. _DF_WHITELIST = ( "corr", "cov", @@ -85,7 +72,7 @@ class DecisionMatrixStatsAccessor(AccessorABC): "var", ) - _DEFAULT_KIND = "describe" + _default_kind = "describe" def __init__(self, dm): self._dm = dm diff --git a/skcriteria/utils/accabc.py b/skcriteria/utils/accabc.py index 9262b84..1d0d0ec 100644 --- a/skcriteria/utils/accabc.py +++ b/skcriteria/utils/accabc.py @@ -11,22 +11,47 @@ """Accessor base class.""" +# ============================================================================= +# IMPORTS +# ============================================================================= + +import abc + # ============================================================================= # ACESSOR ABC # ============================================================================= +# This constans are used to mark a class attribute as abstract, and prevet an instantiaiton of a class +_ABSTRACT = property(abc.abstractmethod(lambda: ...)) + + +class AccessorABC(abc.ABC): + """Generalization of the accessor idea for use in scikit-criteria. + + Instances of this class are callable and accept as the first + parameter 'kind' the name of a method to be executed followed by all the + all the parameters of this method. + + If 'kind' is None, the method defined in the class variable + '_default_kind_kind' is used. + + The last two considerations are that 'kind', cannot be a private method and + that all subclasses of the method and that all AccessorABC subclasses have + to redefine '_default_kind'. -class AccessorABC: + """ - _DEFAULT_KIND = None + #: Default method to execute. + _default_kind = _ABSTRACT - def __init_subclass__(cls) -> None: - if cls._DEFAULT_KIND is None: - raise TypeError(f"{cls!r} must define a _DEFAULT_KIND") + def __init_subclass__(cls): + """Validate the creation of a subclass.""" + if cls._default_kind is _ABSTRACT: + raise TypeError(f"{cls!r} must define a _default_kind") def __call__(self, kind=None, **kwargs): - """""" - kind = self._DEFAULT_KIND if kind is None else kind + """x.__call__() <==> x().""" + kind = self._default_kind if kind is None else kind if kind.startswith("_"): raise ValueError(f"invalid kind name '{kind}'") diff --git a/tests/utils/test_accabc.py b/tests/utils/test_accabc.py index 4c21ec6..3aee43b 100644 --- a/tests/utils/test_accabc.py +++ b/tests/utils/test_accabc.py @@ -34,7 +34,7 @@ def test_AccessorABC(): class FooAccessor(AccessorABC): - _DEFAULT_KIND = "zaraza" + _default_kind = "zaraza" def __init__(self, v): self._v = v @@ -46,17 +46,20 @@ def zaraza(self): assert acc("zaraza") == acc.zaraza() == acc() -def test_AccessorABC_no__DEFAULT_KIND(): +def test_AccessorABC_no__default_kind(): with pytest.raises(TypeError): class FooAccessor(AccessorABC): pass + with pytest.raises(TypeError): + AccessorABC() + def test_AccessorABC_invalid_kind(): class FooAccessor(AccessorABC): - _DEFAULT_KIND = "zaraza" + _default_kind = "zaraza" def __init__(self): self.dont_work = None From cd778525ab86a61a6900a7ca869354c85873bf28 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 01:27:09 -0300 Subject: [PATCH 40/47] docs! --- skcriteria/core/dominance.py | 21 ++++++++++++++++++++- tests/core/test_dominance.py | 9 +++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 6510699..30f4d6c 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -176,8 +176,27 @@ def compute_cell(a0, a1): return self._create_frame(compute_cell) - def resume(self, a0, a1): + # COMPARISONS ============================================================= + def compare(self, a0, a1): + """Compare two alternatives. + + It creates a summary data frame containing the comparison of the two + alternatives on a per-criteria basis, indicating which of the two is + the best value, or if they are equal. In addition, it presents a + "Performance" column with the count for each case. + + Parameters + ---------- + a0, a1: str + Names of the alternatives to compare. + + Returns + ------- + pandas.DataFrame: + Comparison of the two alternatives by criteria. + + """ # read the cache and extract the values centry, ckreverted = self._cache_read(a0, a1) performance_a0, performance_a1 = ( diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py index 5d5946f..33ec0cf 100644 --- a/tests/core/test_dominance.py +++ b/tests/core/test_dominance.py @@ -146,11 +146,11 @@ def test_DecisionMatrixDominanceAccessor_eq(): # ============================================================================= -# RESUME +# COMPARE # ============================================================================= -def test_DecisionMatrixDominanceAccessor_resume(): +def test_DecisionMatrixDominanceAccessor_compare(): dm = data.mkdm( matrix=[ [10, 70], @@ -175,7 +175,7 @@ def test_DecisionMatrixDominanceAccessor_resume(): ("Alternatives", "A1"): False, ("Equals", ""): True, }, - ("Performance", ""): { + ("Better-than", ""): { ("Alternatives", "A0"): 0, ("Alternatives", "A1"): 1, ("Equals", ""): 1, @@ -183,7 +183,8 @@ def test_DecisionMatrixDominanceAccessor_resume(): } ) - result = dom.resume("A0", "A1") + result = dom.compare("A0", "A1") + import ipdb; ipdb.set_trace() pd.testing.assert_frame_equal(result, expected) From 7f61c980944e09bb765b47fa61d03c678ee536bf Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 01:53:00 -0300 Subject: [PATCH 41/47] documentation of dominance --- skcriteria/core/dominance.py | 57 ++++++++++++++++++++++++++++++++++++ skcriteria/utils/accabc.py | 3 +- tests/core/test_dominance.py | 3 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 30f4d6c..9282f66 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -109,6 +109,7 @@ def bt(self): row alternative is better than the column alternative. """ + def compute_cell(a0, a1): if a0 == a1: return 0 @@ -159,6 +160,7 @@ def dominance(self, strict=False): dominates the column alternative. """ + def compute_cell(a0, a1): if a0 == a1: return False @@ -236,12 +238,45 @@ def compare(self, a0, a1): return df + # The dominated============================================================ + def dominated(self, strict=False): + """Which alternative is dominated or strictly dominated by at least \ + one other alternative. + + Parameters + ---------- + strict: bool, default ``False`` + If True, strict dominance is evaluated. + + Returns + ------- + pandas.Series: + Where the index indicates the name of the alternative, and if the + value is is True, it indicates that this alternative is dominated + by at least one other alternative. + + """ return self.dominance(strict=strict).any() @functools.lru_cache(maxsize=None) def dominators_of(self, a, strict=False): + """Array of alternatives that dominate or strictly-dominate the \ + alternative provided by parameters. + + Parameters + ---------- + a : str + On what alternative to look for the dominators. + strict: bool, default ``False`` + If True, strict dominance is evaluated. + Returns + ------- + numpy.ndarray: + List of alternatives that dominate ``a``. + + """ dominance_a = self.dominance(strict=strict)[a] if ~dominance_a.any(): return np.array([], dtype=str) @@ -255,6 +290,28 @@ def dominators_of(self, a, strict=False): return dominators def has_loops(self, strict=False): + """Retorna True si la matriz contiene loops de dominacia. + + A loop is defined as if there are alternatives `a0`, `a1` such that + "a0 ≻ a1 ≻ a0" if ``strict=True``, or "a0 ≽ a1 ≽ a0" if + ``strict=False`` + + Parameters + ---------- + strict: bool, default ``False`` + If True, strict dominance is evaluated. + + Returns + ------- + bool: + If True a loop exists. + + Notes + ----- + If the result of this method is True, the ``dominators_of()`` method + raises a ``RecursionError`` for at least one alternative. + + """ # lets put the dominated alternatives last so our while loop will # be shorter by extracting from the tail diff --git a/skcriteria/utils/accabc.py b/skcriteria/utils/accabc.py index 1d0d0ec..c194b84 100644 --- a/skcriteria/utils/accabc.py +++ b/skcriteria/utils/accabc.py @@ -21,7 +21,8 @@ # ACESSOR ABC # ============================================================================= -# This constans are used to mark a class attribute as abstract, and prevet an instantiaiton of a class +# This constans are used to mark a class attribute as abstract, and prevet an +# instantiaiton of a class _ABSTRACT = property(abc.abstractmethod(lambda: ...)) diff --git a/tests/core/test_dominance.py b/tests/core/test_dominance.py index 33ec0cf..aa6b340 100644 --- a/tests/core/test_dominance.py +++ b/tests/core/test_dominance.py @@ -175,7 +175,7 @@ def test_DecisionMatrixDominanceAccessor_compare(): ("Alternatives", "A1"): False, ("Equals", ""): True, }, - ("Better-than", ""): { + ("Performance", ""): { ("Alternatives", "A0"): 0, ("Alternatives", "A1"): 1, ("Equals", ""): 1, @@ -184,7 +184,6 @@ def test_DecisionMatrixDominanceAccessor_compare(): ) result = dom.compare("A0", "A1") - import ipdb; ipdb.set_trace() pd.testing.assert_frame_equal(result, expected) From ed1811b3ffcf34dcb6f5c8ba670f7550b1ca38ce Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 11:33:59 -0300 Subject: [PATCH 42/47] fix #23 --- skcriteria/core/dominance.py | 6 ++-- skcriteria/preprocessing/filters.py | 47 ++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index 9282f66..b7455d4 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -292,9 +292,9 @@ def dominators_of(self, a, strict=False): def has_loops(self, strict=False): """Retorna True si la matriz contiene loops de dominacia. - A loop is defined as if there are alternatives `a0`, `a1` such that - "a0 ≻ a1 ≻ a0" if ``strict=True``, or "a0 ≽ a1 ≽ a0" if - ``strict=False`` + A loop is defined as if there are alternatives `a0`, `a1` and 'a2' such + that "a0 ≻ a1 ≻ a2 ≻ a0" if ``strict=True``, or "a0 ≽ a1 ≽ a2 ≽ a0" + if ``strict=False`` Parameters ---------- diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index e859c86..64ae001 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -612,6 +612,49 @@ def _set_filter(self, arr, cond): class FilterNonDominated(SKCTransformerABC): + """Keeps the non dominated or non strictly-dominated alternatives. + + In order to evaluate the dominance of an alternative *a0* over an + alternative *a1*, the algorithm evaluates that *a0* is better in at + least one criterion and that *a1* is not better in any criterion than + *a0*. In the case that ``strict = True`` it also evaluates that there + are no equal criteria. + + Parameters + ---------- + strict: bool, default ``False`` + If ``True``, strictly dominated alternatives are removed, otherwise all + dominated alternatives are removed. + + Examples + -------- + .. code-block:: pycon + + >>> from skcriteria.preprocess import filters + + >>> dm = skc.mkdm( + ... matrix=[ + ... [7, 5, 35], + ... [5, 4, 26], + ... [5, 6, 28], + ... [1, 7, 30], + ... [5, 8, 30] + ... ], + ... objectives=[max, max, min], + ... alternatives=["PE", "JN", "AA", "MM", "FN"], + ... criteria=["ROE", "CAP", "RI"], + ... ) + + >>> tfm = filters.FilterNonDominated(strict=False) + >>> tfm.transform(dm) + ROE[▲ 1.0] CAP[▲ 1.0] RI[▼ 1.0] + PE 7 5 35 + JN 5 4 26 + AA 5 6 28 + FN 5 8 30 + [4 Alternatives x 3 Criteria] + + """ _skcriteria_parameters = frozenset(["strict"]) @@ -620,7 +663,8 @@ def __init__(self, *, strict=False): @property def strict(self): - """""" + """If the filter must remove the dominated or strictly-dominated \ + alternatives.""" return self._strict @doc_inherit(SKCTransformerABC._transform_data) @@ -635,6 +679,7 @@ def _transform_data(self, matrix, alternatives, dominated_mask, **kwargs): ) return kwargs + @doc_inherit(SKCTransformerABC.transform) def transform(self, dm): data = dm.to_dict() From 60c8fb0e1ed31f8a51ce518768e245eb290487b5 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 11:57:32 -0300 Subject: [PATCH 43/47] simplified _skcriteria_parameters --- skcriteria/preprocessing/filters.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skcriteria/preprocessing/filters.py b/skcriteria/preprocessing/filters.py index 64ae001..fb418b9 100644 --- a/skcriteria/preprocessing/filters.py +++ b/skcriteria/preprocessing/filters.py @@ -47,9 +47,8 @@ class SKCByCriteriaFilterABC(SKCTransformerABC): """ - _skcriteria_parameters = frozenset( - ["criteria_filters", "ignore_missing_criteria"] - ) + _skcriteria_parameters = ["criteria_filters", "ignore_missing_criteria"] + _skcriteria_abstract_class = True def __init__(self, criteria_filters, *, ignore_missing_criteria=False): @@ -656,7 +655,7 @@ class FilterNonDominated(SKCTransformerABC): """ - _skcriteria_parameters = frozenset(["strict"]) + _skcriteria_parameters = ["strict"] def __init__(self, *, strict=False): self._strict = bool(strict) From 0cd9099455b6e45659347dc08e6eeec95f876c10 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Thu, 17 Feb 2022 12:07:49 -0300 Subject: [PATCH 44/47] 3.10 in tox --- .github/workflows/tests.yml | 2 +- tox.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d3151c..c0742e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - tox_env: [py37, py38, py39] + tox_env: [py37, py38, py39, py310] include: - tox_env: style - tox_env: docstyle diff --git a/tox.ini b/tox.ini index 0d8b639..e359b6a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ envlist = py37, py38, py39, + py10, coverage # ============================================================================= From 271ca504d5eeed3d5e7128972c80f588bc9e799f Mon Sep 17 00:00:00 2001 From: JuanBC Date: Sun, 20 Feb 2022 22:21:29 -0300 Subject: [PATCH 45/47] still need translation of sufdom --- docs/requirements.txt | 2 + docs/source/conf.py | 1 + docs/source/refs.bib | 13 + docs/source/tutorial/index.rst | 4 +- docs/source/tutorial/quickstart.ipynb | 9 +- docs/source/tutorial/sufdom.ipynb | 1365 +++++++++++++++++++++++++ 6 files changed, 1391 insertions(+), 3 deletions(-) create mode 100644 docs/source/tutorial/sufdom.ipynb diff --git a/docs/requirements.txt b/docs/requirements.txt index 5f5d53a..295b167 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,6 +2,8 @@ Sphinx sphinx_rtd_theme nbsphinx +sphinx_copybutton + ipykernel ipython diff --git a/docs/source/conf.py b/docs/source/conf.py index 0c91cc1..8eeb311 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -57,6 +57,7 @@ "sphinx.ext.autosummary", "nbsphinx", "sphinxcontrib.bibtex", + "sphinx_copybutton" ] # ============================================================================= # EXTRA CONF diff --git a/docs/source/refs.bib b/docs/source/refs.bib index 4865434..ee87afd 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -119,4 +119,17 @@ @article{diakoulaki1995determining pages = {763--770}, year = {1995}, publisher = {Elsevier} +} + +% tutorial + +@article{simon1955behavioral, + title = {A behavioral model of rational choice}, + author = {Simon, Herbert A}, + journal = {The quarterly journal of economics}, + volume = {69}, + number = {1}, + pages = {99--118}, + year = {1955}, + publisher = {MIT Press} } \ No newline at end of file diff --git a/docs/source/tutorial/index.rst b/docs/source/tutorial/index.rst index cdcfd90..bc5bc4a 100644 --- a/docs/source/tutorial/index.rst +++ b/docs/source/tutorial/index.rst @@ -8,9 +8,9 @@ Contents: .. toctree:: :maxdepth: 2 - :glob: - * + quickstart.ipynb + sufdom.ipynb diff --git a/docs/source/tutorial/quickstart.ipynb b/docs/source/tutorial/quickstart.ipynb index 94c82f9..27eda2b 100644 --- a/docs/source/tutorial/quickstart.ipynb +++ b/docs/source/tutorial/quickstart.ipynb @@ -1574,6 +1574,13 @@ "As can be seen for this case both scalings give the same results" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----" + ] + }, { "cell_type": "code", "execution_count": 35, @@ -1617,7 +1624,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.9.10" } }, "nbformat": 4, diff --git a/docs/source/tutorial/sufdom.ipynb b/docs/source/tutorial/sufdom.ipynb new file mode 100644 index 0000000..4352efb --- /dev/null +++ b/docs/source/tutorial/sufdom.ipynb @@ -0,0 +1,1365 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dominance and satisfaction anlysis (AKA filters)\n", + "\n", + "Se ejemplifica a continuación un ejemplo práctico de como filtrar alternativas\n", + "que sean dominadas, o que \n", + "\n", + "\n", + "## Conceptual overview\n", + "\n", + "A fin de decidir comprar una serie de bonos, una empresa estudió cinco \n", + "inversiones candidatas: *PE*, *JN*, *AA*, *FX*, *MM* y *GN*. \n", + "El departamento de finanzas decide considerar los siguientes criterios para la \n", + "selección:\n", + "\n", + "1. **ROE:** Rendimiento porcentual por cada peso invertido. Sentido de optimidad, $Maximize$.\n", + "2. **CAP:** Años de capitalización en el mercado. Sentido de optimidad, $Maximize$. \n", + "3. **RI:** Puntos de riesgo del título valor. Sentido de optimidad, $Minimize$. \n", + "\n", + "The full decision matrix" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "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", + " ROE[▲ 1.0]CAP[▲ 1.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
FX3436
MM1730
FN5830
\n", + "
6 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 1.0] CAP[▲ 1.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "JN 5 4 26\n", + "AA 5 6 28\n", + "FX 3 4 36\n", + "MM 1 7 30\n", + "FN 5 8 30\n", + "[6 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import skcriteria as skc\n", + "\n", + "dm = skc.mkdm(\n", + " matrix=[\n", + " [7, 5, 35],\n", + " [5, 4, 26],\n", + " [5, 6, 28],\n", + " [3, 4, 36],\n", + " [1, 7, 30],\n", + " [5, 8, 30],\n", + " ],\n", + " objectives=[max, max, min],\n", + " alternatives=[\"PE\", \"JN\", \"AA\", \"FX\", \"MM\", \"FN\"],\n", + " criteria=[\"ROE\", \"CAP\", \"RI\"],\n", + ")\n", + "\n", + "dm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Satisfaction analysis\n", + "\n", + "Es razonable pensar que cualquier decisor quiera fijar “umbrales de \n", + "satisfacción” para cada criterio, de manera tal \n", + "que se eliminen las alternativas que en algún criterio no los superan.\n", + "\n", + "La idea básica fue propuesta en el trabajo \n", + "\"_A Behavioral Model of Rational Choice_\" \n", + "[simon1955behavioral], que habla \n", + "de “*niveles de aspiración*” y son fijados a priori por el decisor.\n", + "\n", + "> Para nuestro ejemplo supondremos que el decisor solo se acepta alternativas \n", + "> que rindan al menos el 2%\n", + "\n", + "Por esto vamos a necesitar el modulo `filters`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from skcriteria.preprocessing import filters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Los filtros operan de la siguiente manera:\n", + "\n", + "- Al momento de contrucción se les probee de un dict que como llave tiene\n", + " el nombre de un criterio, y como valor la condiccion que debe satisfacer\n", + " el criterio para no ser elmiminaado.\n", + "- Opcionalmente recibe un parametro `ignore_missing_criteria` el cual si esta\n", + " en False (valor por defecto) hace fallar cualquier intento de transformacion\n", + " de una matriz que no tenga alguno de los criterios .\n", + "- Para que una alternativa no sea eliminada la alternativa tiene que pasar \n", + " todas las condiciones del filtro.\n", + "\n", + "El filtro mas simple, consiste los objetos de la clase ``filters.Filter``\n", + "los cuales como valor del map a satisfacer, reciben una funcion que recibe\n", + "como parámetro el criterio a evaluar y retorna una mascara con valores `True`\n", + "para los criterios que queremos mantener.\n", + "\n", + "Para escribir la funcion que filtre *ROE* para si su valor es >= que 2%." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def roe_filter(v):\n", + " return v >= 2 # como v es un numpy array esto es bastante corto" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ahora si queremos incorporarlo al filtro, el código quedaria de la siguiente\n", + "forma" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Filter(criteria_filters={'ROE': }, ignore_missing_criteria=False)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flt = filters.Filter({\"ROE\": roe_filter})\n", + "flt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "De todas formas `scikit-criteria` ofrece una coleccion de filtros mas simple\n", + "que implementas las operaciones mas comunes de igualdad, desigualdad\n", + "e inclusion en un conjunto.\n", + "\n", + "En nuestro claso estamos interesados en la clase `FilterGE`, donde GE significa\n", + "*Greater or Equal*.\n", + "\n", + "Asi el filtro quedaria definido como" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FilterGE(criteria_filters={'ROE': 2}, ignore_missing_criteria=False)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flt = filters.FilterGE({\"ROE\": 2})\n", + "flt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La forma de aplicar el filtro a una DecisionMatrix, es como cualquier \n", + "transformador: utilizando el metodo transform:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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", + " ROE[▲ 1.0]CAP[▲ 1.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
FX3436
FN5830
\n", + "
5 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 1.0] CAP[▲ 1.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "JN 5 4 26\n", + "AA 5 6 28\n", + "FX 3 4 36\n", + "FN 5 8 30\n", + "[5 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dmf = flt.transform(dm)\n", + "dmf" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Como se puede, verse se elimino la alternativa `MM` la cual no cumplia con un\n", + "$ROE >= 2$.\n", + "\n", + "Si por otro lado (por poner un ejemplo) quisieramos filtrar las alternativas\n", + "$ROE > 3$ y $CAP > 4$ (utilizando la matriz original), podemos utilizar\n", + "el filtro `FilterGT` donde GT es *Greater Than*." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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", + " ROE[▲ 1.0]CAP[▲ 1.0]RI[▼ 1.0]
PE7535
AA5628
FN5830
\n", + "
3 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 1.0] CAP[▲ 1.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "AA 5 6 28\n", + "FN 5 8 30\n", + "[3 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "filters.FilterGT({\"ROE\": 3, \"CAP\": 4}).transform(dm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "**Note:** \n", + "\n", + "Si es necesario filtrar las alternativas por dos condiciones separadas \n", + "puede utilizarse un pipeline. Un ejemplo de esto podemos verlo mas adelante.\n", + "
\n", + "\n", + "El listado completo de filtros implementados, por Scikit-Criteria es:\n", + "\n", + "- ``filters.Filter``: Filter alternatives according to the value of a criterion \n", + " using arbitrary functions.\n", + "\n", + " ```python\n", + " filters.Filter({\"criterion\": lambda v: v > 1})\n", + " ```\n", + "\n", + "- ``filters.FilterGT``: Filter Greater Than ($>$).\n", + " \n", + " ```python\n", + " filters.FilterGT({\"criterion\": 1})\n", + " ```\n", + "\n", + "- ``filters.FilterGE``: Filter Greater or Equal than ($>=$).\n", + "\n", + " ```python\n", + " filters.FilterGE({\"criterion\": 2})\n", + " ```\n", + "\n", + "- ``filters.FilterLT``: Filter Less Than ($<$).\n", + " \n", + " ```python\n", + " filters.FilterLT({\"criterion\": 1})\n", + " ```\n", + "\n", + "- ``filters.FilterLE``: Filter Less or Equal than ($<=$).\n", + "\n", + " ```python\n", + " filters.FilterLE({\"criterion\": 2})\n", + " ```\n", + "\n", + "- ``filters.FilterEQ``: Filter Equal ($==$).\n", + " \n", + " ```python\n", + " filters.FilterEQ({\"criterion\": 1})\n", + " ```\n", + "\n", + "- ``filters.FilterNE``: Filter Not-Equal than ($!=$).\n", + "\n", + " ```python\n", + " filters.FilterNE({\"criterion\": 2})\n", + " ```\n", + "\n", + "- ``filters.FilterIn``: Filter if the values is in a set ($\\in$).\n", + " \n", + " ```python\n", + " filters.FilterIn({\"criterion\": [1, 2, 3]})\n", + " ```\n", + "\n", + "- ``filters.FilterNotIn``: Filter if the values is not in a set ($\\notin$).\n", + "\n", + " ```python\n", + " filters.FilterNotIn({\"criterion\": [1, 2, 3]})\n", + " ```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dominance\n", + "\n", + "Se dice que que una alternativa $A_0$ domina a una alternativa $A_1$ \n", + "($A_0 \\succeq A_1$), si $A_0$ es igual en todos los criterios y mejor en \n", + "almenos un criterio. Por otro lado, se dice que $A_0$ domina estrictiamente \n", + "$A_1$ ($A_0 \\succ A_1$), si $A_0$ es mejor en todos los criterios que $A_1$.\n", + "\n", + "Bajo este mismo tren de pnesamiento, se denomina *alternativa dominante* \n", + "a una alternativa que domina a todas las demas.\n", + "\n", + "Si existe una alternativa dominante es sin duda la mejor elección. Si no se \n", + "quiere de una ordenación consignada, ya se tiene la solución del problema. \n", + "\n", + "Por otro lado, una *alternativa es dominada* si existe al menos otra alternativa\n", + "que la domina. Si existe una alternativa dominada y no se quiere una ordenación \n", + "consignada, debe ser apartada del conjunto de alternativas de decisión. \n", + "\n", + "Generalmente sólo las alternativas no-dominadas o eficientes son las que interesan.\n", + "\n", + "### Scikit-Criteria dominance analysis\n", + "\n", + "Scikit-criteria, contiene una serie de herramientas dentro del atributo,\n", + "`dominance` de decision matrix, útil para la evaluacion de alternativas\n", + "dominantes y dominadas.\n", + "\n", + "Por ejemplo podemos acceder a todas las alternativas dominadas utilizando\n", + "el ḿetodo `dominated`" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PE False\n", + "JN False\n", + "AA False\n", + "FX True\n", + "FN False\n", + "dtype: bool" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dmf.dominance.dominated()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Se puede ver con esto, que `FX` es una alternativa, dominada. Si por otro\n", + "lafo queremos saber cuales son las alternativas *estrictamente dominadas* \n", + "se realizando proveyendo el parámetro `strict`al método" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PE False\n", + "JN False\n", + "AA False\n", + "FX True\n", + "FN False\n", + "dtype: bool" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dmf.dominance.dominated(strict=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Puede verse que FX es estrictamente dominado por almenos otra alternativa.\n", + "\n", + "Si quisieramos averiguar cuales son las alternativas dominantes de *FX*, \n", + "podemos optar por dos caminos: \n", + "\n", + "1. Listar todas las alternativas dominantes/estrictamente dominantes de \n", + "2. *FX* utiizatilizando `dominator_of()`." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['PE', 'AA', 'FN'], dtype=object)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dmf.dominance.dominators_of(\"FX\", strict=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Utilizar `dominance()`/`dominance.dominance()` para ver la relación completa\n", + " de dominancia entre todas alternativas. " + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "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", + "
PEJNAAFXFN
PEFalseFalseFalseTrueFalse
JNFalseFalseFalseFalseFalse
AAFalseFalseFalseTrueFalse
FXFalseFalseFalseFalseFalse
FNFalseFalseFalseTrueFalse
\n", + "
" + ], + "text/plain": [ + " PE JN AA FX FN\n", + "PE False False False True False\n", + "JN False False False False False\n", + "AA False False False True False\n", + "FX False False False False False\n", + "FN False False False True False" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dmf.dominance(strict=True) # equivalent to dmf.dominance.dominance()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "En este caso lo que se observa es que alternativa Fila domina a que alternativa\n", + "columna.\n", + "\n", + "Podemos estudiar esta matriz en forma de heamap usando, por ejemplo, la \n", + "libreria [seaborn](http://seaborn.pydata.org/)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import seaborn as sns\n", + "sns.heatmap(dmf.dominance.dominance(strict=True), cmap=\"magma_r\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finalmente podemos ver como se relacionan cada una de las alternativas \n", + "dominatnes con *FX* utilizando `compare()`" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "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", + "
CriteriaPerformance
ROECAPRI
AlternativesPETrueTrueTrue3
FXFalseFalseFalse0
EqualsFalseFalseFalse0
\n", + "
" + ], + "text/plain": [ + " Criteria Performance\n", + " ROE CAP RI \n", + "Alternatives PE True True True 3\n", + " FX False False False 0\n", + "Equals False False False 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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", + "
CriteriaPerformance
ROECAPRI
AlternativesJNTrueFalseTrue2
FXFalseFalseFalse0
EqualsFalseTrueFalse1
\n", + "
" + ], + "text/plain": [ + " Criteria Performance\n", + " ROE CAP RI \n", + "Alternatives JN True False True 2\n", + " FX False False False 0\n", + "Equals False True False 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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", + "
CriteriaPerformance
ROECAPRI
AlternativesAATrueTrueTrue3
FXFalseFalseFalse0
EqualsFalseFalseFalse0
\n", + "
" + ], + "text/plain": [ + " Criteria Performance\n", + " ROE CAP RI \n", + "Alternatives AA True True True 3\n", + " FX False False False 0\n", + "Equals False False False 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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", + "
CriteriaPerformance
ROECAPRI
AlternativesFNTrueTrueTrue3
FXFalseFalseFalse0
EqualsFalseFalseFalse0
\n", + "
" + ], + "text/plain": [ + " Criteria Performance\n", + " ROE CAP RI \n", + "Alternatives FN True True True 3\n", + " FX False False False 0\n", + "Equals False False False 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for dominant in dmf.dominance.dominators_of(\"FX\"):\n", + " display(dmf.dominance.compare(dominant, 'FX'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filter non-dominated alternatives\n", + "\n", + "Finalmente skcriteria ofrece una forma de filtar alternativas non-dominadas,\n", + "la cual acepta como parametro si quiere evaluar la dominancia estrictca." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FilterNonDominated(strict=True)" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flt = filters.FilterNonDominated(strict=True)\n", + "flt" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "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", + " ROE[▲ 1.0]CAP[▲ 1.0]RI[▼ 1.0]
PE7535
JN5426
AA5628
FN5830
\n", + "
4 Alternatives x 3 Criteria\n", + "
" + ], + "text/plain": [ + " ROE[▲ 1.0] CAP[▲ 1.0] RI[▼ 1.0]\n", + "PE 7 5 35\n", + "JN 5 4 26\n", + "AA 5 6 28\n", + "FN 5 8 30\n", + "[4 Alternatives x 3 Criteria]" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flt.transform(dmf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Full expermient\n", + "\n", + "Podemos finalmente crear un experimento completo de MCDA que tome en cuenta\n", + "las en análisis de satisfaccion y dominancia.\n", + "\n", + "El expeimento completo tendria las siguientes etapas\n", + "\n", + "1. Se eliminan alternativas que no que rindan al menos el 2% ($ROE >= 2$).\n", + "2. Se eliminan alternativas dominadas.\n", + "3. Se convierten todos los criterios a maximizar.\n", + "4. Se excala los pesos por la suma todal.\n", + "5. Se escala la matriz por el modulo del vecto.\n", + "6. Se aplica [TOPSIS]()\n", + "\n", + "Para esto lo mas conveniente es utilizar un pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SKCPipeline(steps=[('filterge', FilterGE(criteria_filters={'ROE': 2}, ignore_missing_criteria=False)), ('filternondominated', FilterNonDominated(strict=True)), ('minimizetomaximize', MinimizeToMaximize()), ('sumscaler', SumScaler(target='weights')), ('vectorscaler', VectorScaler(target='matrix')), ('topsis', TOPSIS(metric='euclidean'))])" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from skcriteria.preprocessing import scalers, invert_objectives\n", + "from skcriteria.madm.similarity import TOPSIS\n", + "from skcriteria.pipeline import mkpipe\n", + "\n", + "pipe = mkpipe(\n", + " filters.FilterGE({\"ROE\": 2}),\n", + " filters.FilterNonDominated(strict=True),\n", + " invert_objectives.MinimizeToMaximize(),\n", + " scalers.SumScaler(target=\"weights\"),\n", + " scalers.VectorScaler(target=\"matrix\"),\n", + " TOPSIS(),\n", + ")\n", + "\n", + "pipe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ahora aplicamos el pipeline a los datos originales" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "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", + "
 PEJNAAFN
Rank3421
\n", + "Method: TOPSIS\n", + "
" + ], + "text/plain": [ + " PE JN AA FN\n", + "Rank 3 4 2 1\n", + "[Method: TOPSIS]" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pipe.evaluate(dm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime as dt\n", + "import skcriteria\n", + "\n", + "print(\"Scikit-Criteria version:\", skcriteria.VERSION)\n", + "print(\"Running datetime:\", dt.datetime.now())" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "interpreter": { + "hash": "0a948a28a0ef94db8982fe8442467cdefb8d68ae66a4b8f3824263fcc702cc89" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "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.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 9859620588c694f893d9991e71ce31af013e18a2 Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 21 Feb 2022 15:28:51 -0300 Subject: [PATCH 46/47] 0.6.dev0 --- docs/source/tutorial/index.rst | 2 +- docs/source/tutorial/sufdom.ipynb | 223 +++++++++++++++--------------- setup.py | 3 +- skcriteria/core/dominance.py | 8 +- 4 files changed, 115 insertions(+), 121 deletions(-) diff --git a/docs/source/tutorial/index.rst b/docs/source/tutorial/index.rst index bc5bc4a..6a32a63 100644 --- a/docs/source/tutorial/index.rst +++ b/docs/source/tutorial/index.rst @@ -7,7 +7,7 @@ scikit-criteria Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 quickstart.ipynb sufdom.ipynb diff --git a/docs/source/tutorial/sufdom.ipynb b/docs/source/tutorial/sufdom.ipynb index 4352efb..0634734 100644 --- a/docs/source/tutorial/sufdom.ipynb +++ b/docs/source/tutorial/sufdom.ipynb @@ -4,22 +4,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Dominance and satisfaction anlysis (AKA filters)\n", + "# Dominance and satisfaction analysis (AKA filters)\n", "\n", - "Se ejemplifica a continuación un ejemplo práctico de como filtrar alternativas\n", - "que sean dominadas, o que \n", + "This tutorial provides a practical overview of how to use scikit-criteria for \n", + "satisfaction and dominance analysis, as well as the creation of filters for \n", + "data cleaning.\n", "\n", "\n", - "## Conceptual overview\n", + "## Case\n", "\n", - "A fin de decidir comprar una serie de bonos, una empresa estudió cinco \n", - "inversiones candidatas: *PE*, *JN*, *AA*, *FX*, *MM* y *GN*. \n", - "El departamento de finanzas decide considerar los siguientes criterios para la \n", - "selección:\n", + "In order to decide to purchase a series of bonds, a company studied five \n", + "candidate investments: *PE*, *JN*, *AA*, *FX*, *MM* and *GN*. \n", "\n", - "1. **ROE:** Rendimiento porcentual por cada peso invertido. Sentido de optimidad, $Maximize$.\n", - "2. **CAP:** Años de capitalización en el mercado. Sentido de optimidad, $Maximize$. \n", - "3. **RI:** Puntos de riesgo del título valor. Sentido de optimidad, $Minimize$. \n", + "The finance department decides to consider the following criteria for selection. \n", + "selection:\n", + "\n", + "1. **ROE:** Return percentage. Sense of optimality, \n", + " $Maximize$.\n", + "2. **CAP:** Market capitalization. Sense of optimality, $Maximize$. \n", + "3. **RI:** Risk. Sense of optimality, $Minimize$. \n", "\n", "The full decision matrix" ] @@ -140,19 +143,20 @@ "source": [ "## Satisfaction analysis\n", "\n", - "Es razonable pensar que cualquier decisor quiera fijar “umbrales de \n", - "satisfacción” para cada criterio, de manera tal \n", - "que se eliminen las alternativas que en algún criterio no los superan.\n", + "It is reasonable to think that any decision-maker would want to set \n", + "\"satisfaction thresholds\" for each criterion, in such a way that alternatives \n", + "that do not exceed the thresholds in any criterion are eliminated.\n", + "\n", + "The basic idea was proposed in the work of \n", + "\"_A Behavioral Model of Rational Choice_\" \n", + "[simon1955behavioral] and presents \n", + "the definition of \"*aspiration levels*\" and are set a priori by the decision maker.\n", "\n", - "La idea básica fue propuesta en el trabajo \n", - "\"_A Behavioral Model of Rational Choice_\" \n", - "[simon1955behavioral], que habla \n", - "de “*niveles de aspiración*” y son fijados a priori por el decisor.\n", "\n", - "> Para nuestro ejemplo supondremos que el decisor solo se acepta alternativas \n", - "> que rindan al menos el 2%\n", + "> For our example we will assume that the decision-maker only accepts alternatives \n", + "> with $ROE >= 2%$.\n", "\n", - "Por esto vamos a necesitar el modulo `filters`." + "For this analysis we will need the `skcriteria.preprocessing.filters` module ." ] }, { @@ -168,60 +172,44 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Los filtros operan de la siguiente manera:\n", + "The filters are *transformers* and works as follows:\n", "\n", - "- Al momento de contrucción se les probee de un dict que como llave tiene\n", - " el nombre de un criterio, y como valor la condiccion que debe satisfacer\n", - " el criterio para no ser elmiminaado.\n", - "- Opcionalmente recibe un parametro `ignore_missing_criteria` el cual si esta\n", - " en False (valor por defecto) hace fallar cualquier intento de transformacion\n", - " de una matriz que no tenga alguno de los criterios .\n", - "- Para que una alternativa no sea eliminada la alternativa tiene que pasar \n", - " todas las condiciones del filtro.\n", + "- At the moment of construction they are provided with a dict that as a key has \n", + " the name of a criterion, and as a value the condition to be satisfied.\n", + "- Optionally it receives a parameter `ignore_missing_criteria` which if it is \n", + " set to False (default value) fails any attempt to transform an decision \n", + " matrix that does not have any of the criteria.\n", + "- For an alternative not to be eliminated the alternative has to pass all \n", + " filter conditions. \n", "\n", - "El filtro mas simple, consiste los objetos de la clase ``filters.Filter``\n", - "los cuales como valor del map a satisfacer, reciben una funcion que recibe\n", - "como parámetro el criterio a evaluar y retorna una mascara con valores `True`\n", - "para los criterios que queremos mantener.\n", + "The simplest filter consists of instances of the class ``filters.Filters``,\n", + "which as a value of the configuration dict, accepts functions that are applied \n", + "to the corresponding criteria and returns a mask where the `True` values denote \n", + "the alternatives that we want to keep.\n", "\n", - "Para escribir la funcion que filtre *ROE* para si su valor es >= que 2%." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def roe_filter(v):\n", - " return v >= 2 # como v es un numpy array esto es bastante corto" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ahora si queremos incorporarlo al filtro, el código quedaria de la siguiente\n", - "forma" + "To write the function that filters the alternatives where $ROE >= 2." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 52, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Filter(criteria_filters={'ROE': }, ignore_missing_criteria=False)" + "Filter(criteria_filters={'ROE': }, ignore_missing_criteria=False)" ] }, - "execution_count": 4, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "def roe_filter(v):\n", + " return v >= 2 # criteria are numpy.ndarray\n", + "\n", "flt = filters.Filter({\"ROE\": roe_filter})\n", "flt" ] @@ -230,19 +218,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "De todas formas `scikit-criteria` ofrece una coleccion de filtros mas simple\n", - "que implementas las operaciones mas comunes de igualdad, desigualdad\n", - "e inclusion en un conjunto.\n", + "However, `scikit-criteria` offers a simpler collection of filters\n", + "that implements the most common operations of equality, inequality and \n", + "inclusion a set.\n", "\n", - "En nuestro claso estamos interesados en la clase `FilterGE`, donde GE significa\n", + "In our case we are interested in the `FilterGE` class, where GE stands for\n", "*Greater or Equal*.\n", "\n", - "Asi el filtro quedaria definido como" + "So the filter would be defined as" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 53, "metadata": {}, "outputs": [ { @@ -251,7 +239,7 @@ "FilterGE(criteria_filters={'ROE': 2}, ignore_missing_criteria=False)" ] }, - "execution_count": 5, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -265,13 +253,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "La forma de aplicar el filtro a una DecisionMatrix, es como cualquier \n", - "transformador: utilizando el metodo transform:" + "The way to apply the filter to a `DecisionMatrix`, is like any other \n", + "transformer: " ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 54, "metadata": {}, "outputs": [ { @@ -347,7 +335,7 @@ "[5 Alternatives x 3 Criteria]" ] }, - "execution_count": 6, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -361,17 +349,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Como se puede, verse se elimino la alternativa `MM` la cual no cumplia con un\n", + "As can be seen, we eliminated the alternative `MM` which did not comply with an\n", "$ROE >= 2$.\n", "\n", - "Si por otro lado (por poner un ejemplo) quisieramos filtrar las alternativas\n", - "$ROE > 3$ y $CAP > 4$ (utilizando la matriz original), podemos utilizar\n", - "el filtro `FilterGT` donde GT es *Greater Than*." + "If on the other hand (to give an example) we would like to filter out the alternatives\n", + "$ROE > 3$ and $CAP > 4$ (using the original matrix), we can use the \n", + "filter `FilterGT` where GT is *Greater Than*." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 55, "metadata": {}, "outputs": [ { @@ -433,7 +421,7 @@ "[3 Alternatives x 3 Criteria]" ] }, - "execution_count": 7, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -449,11 +437,12 @@ "
\n", "**Note:** \n", "\n", - "Si es necesario filtrar las alternativas por dos condiciones separadas \n", - "puede utilizarse un pipeline. Un ejemplo de esto podemos verlo mas adelante.\n", + "If it is necessary to filter the alternatives by two separate conditions, \n", + "a pipeline can be used. An example of this can be seen below, where we combine \n", + "a satisficing and a dominance filter\n", "
\n", "\n", - "El listado completo de filtros implementados, por Scikit-Criteria es:\n", + "The complete list of filters implemented by Scikit-Criteria is:\n", "\n", "- ``filters.Filter``: Filter alternatives according to the value of a criterion \n", " using arbitrary functions.\n", @@ -517,31 +506,33 @@ "source": [ "## Dominance\n", "\n", - "Se dice que que una alternativa $A_0$ domina a una alternativa $A_1$ \n", - "($A_0 \\succeq A_1$), si $A_0$ es igual en todos los criterios y mejor en \n", - "almenos un criterio. Por otro lado, se dice que $A_0$ domina estrictiamente \n", - "$A_1$ ($A_0 \\succ A_1$), si $A_0$ es mejor en todos los criterios que $A_1$.\n", + "An alternative $A_0$ is said to dominate an alternative $A_1$ \n", + "($A_0 \\succeq A_1$), if $A_0$ is equal in all criteria and better in at least \n", + "one criterion. On the other hand, $A_0$ strictly dominate $A_1$ \n", + "($A_0$ \\succeq A_1$). $A_1$ ($A_0 \\succ A_1$), if $A_0$ is better on all \n", + "criteria than $A_1$.\n", "\n", - "Bajo este mismo tren de pnesamiento, se denomina *alternativa dominante* \n", - "a una alternativa que domina a todas las demas.\n", "\n", - "Si existe una alternativa dominante es sin duda la mejor elección. Si no se \n", - "quiere de una ordenación consignada, ya se tiene la solución del problema. \n", + "Under this same train of thought, an alternative that dominates all others is \n", + "called a \"_dominant alternative_\". If there is a dominant alternative, it is \n", + "undoubtedly the best choice, as long as a full ranking is not required.\n", "\n", - "Por otro lado, una *alternativa es dominada* si existe al menos otra alternativa\n", - "que la domina. Si existe una alternativa dominada y no se quiere una ordenación \n", - "consignada, debe ser apartada del conjunto de alternativas de decisión. \n", + "On the other hand, an *alternative is dominated* if there exists at least one \n", + "other alternative that dominates it. If a dominated alternative exists and a \n", + "consigned ordering is not desired, it must be removed from the set of decision \n", + "alternatives. \n", "\n", - "Generalmente sólo las alternativas no-dominadas o eficientes son las que interesan.\n", + "Generally only the non-dominated or efficient alternatives are the interested\n", + " ones.\n", "\n", "### Scikit-Criteria dominance analysis\n", "\n", - "Scikit-criteria, contiene una serie de herramientas dentro del atributo,\n", - "`dominance` de decision matrix, útil para la evaluacion de alternativas\n", - "dominantes y dominadas.\n", + "Scikit-criteria, contains a number of tools within the attribute,\n", + "`DecisionMatrix.dominance`, useful for the evaluation of dominant and dominated \n", + "alternatives.\n", "\n", - "Por ejemplo podemos acceder a todas las alternativas dominadas utilizando\n", - "el ḿetodo `dominated`" + "For example, we can access all the dominated alternatives by using\n", + "the `dominated` method" ] }, { @@ -573,9 +564,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Se puede ver con esto, que `FX` es una alternativa, dominada. Si por otro\n", - "lafo queremos saber cuales son las alternativas *estrictamente dominadas* \n", - "se realizando proveyendo el parámetro `strict`al método" + "It can be seen with this, that `FX` is an dominated alternative. In addition \n", + "if we want to know which are the *strictly dominated* alternatives we need to \n", + "provide the `strict` parameter to the method:" ] }, { @@ -607,13 +598,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Puede verse que FX es estrictamente dominado por almenos otra alternativa.\n", + "It can be seen that *FX* is strictly dominated by at least one other alternative.\n", "\n", - "Si quisieramos averiguar cuales son las alternativas dominantes de *FX*, \n", - "podemos optar por dos caminos: \n", + "If we wanted to find out which are the dominant alternatives of *FX*, \n", + "we can opt for two paths: \n", "\n", - "1. Listar todas las alternativas dominantes/estrictamente dominantes de \n", - "2. *FX* utiizatilizando `dominator_of()`." + "1. List all the dominant/strictly dominated alternatives of \n", + " *FX* using `dominator_of()`." ] }, { @@ -640,8 +631,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "2. Utilizar `dominance()`/`dominance.dominance()` para ver la relación completa\n", - " de dominancia entre todas alternativas. " + "2. Use `dominance()`/`dominance.dominance()` to see the full relationship \n", + " between all alternatives." ] }, { @@ -744,16 +735,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "En este caso lo que se observa es que alternativa Fila domina a que alternativa\n", - "columna.\n", + "the result of the method is a \n", + "[DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) \n", + "that in each cell has a `True` value if the _row alternative_ dominates the \n", + "_column alternative_.\n", "\n", - "Podemos estudiar esta matriz en forma de heamap usando, por ejemplo, la \n", - "libreria [seaborn](http://seaborn.pydata.org/)" + "If this matrix is very large we can, for example, visualize it in the form of \n", + "a *heatmap* using the library [seaborn](http://seaborn.pydata.org/)" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 56, "metadata": {}, "outputs": [ { @@ -762,13 +755,13 @@ "" ] }, - "execution_count": 42, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -781,20 +774,20 @@ ], "source": [ "import seaborn as sns\n", - "sns.heatmap(dmf.dominance.dominance(strict=True), cmap=\"magma_r\")" + "sns.heatmap(dmf.dominance.dominance(strict=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Finalmente podemos ver como se relacionan cada una de las alternativas \n", - "dominatnes con *FX* utilizando `compare()`" + "Finally we can see how each of the alternatives relate to each other \n", + "dominatnes with *FX* using `compare()`." ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 57, "metadata": {}, "outputs": [ { @@ -1222,7 +1215,7 @@ "3. Se convierten todos los criterios a maximizar.\n", "4. Se excala los pesos por la suma todal.\n", "5. Se escala la matriz por el modulo del vecto.\n", - "6. Se aplica [TOPSIS]()\n", + "6. Se aplica [TOPSIS](https://en.wikipedia.org/wiki/TOPSIS).\n", "\n", "Para esto lo mas conveniente es utilizar un pipeline" ] diff --git a/setup.py b/setup.py index cba8f19..7aefd3d 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ def do_setup(): description=skcriteria.DOC, long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", - author="Juan B Cabral, Nadial Luczywo and QuatroPe", + author="QuatroPe", author_email="jbcabral@unc.edu.ar", url="http://scikit-criteria.org/", license="3 Clause BSD", @@ -90,6 +90,7 @@ def do_setup(): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering", ], diff --git a/skcriteria/core/dominance.py b/skcriteria/core/dominance.py index b7455d4..6c584da 100644 --- a/skcriteria/core/dominance.py +++ b/skcriteria/core/dominance.py @@ -138,7 +138,7 @@ def compute_cell(a0, a1): return self._create_frame(compute_cell) - def dominance(self, strict=False): + def dominance(self, *, strict=False): """Compare if one alternative dominates or strictly dominates another \ alternative. @@ -240,7 +240,7 @@ def compare(self, a0, a1): # The dominated============================================================ - def dominated(self, strict=False): + def dominated(self, *, strict=False): """Which alternative is dominated or strictly dominated by at least \ one other alternative. @@ -260,7 +260,7 @@ def dominated(self, strict=False): return self.dominance(strict=strict).any() @functools.lru_cache(maxsize=None) - def dominators_of(self, a, strict=False): + def dominators_of(self, a, *, strict=False): """Array of alternatives that dominate or strictly-dominate the \ alternative provided by parameters. @@ -289,7 +289,7 @@ def dominators_of(self, a, strict=False): dominators = np.concatenate((dominators, dominators_dominators)) return dominators - def has_loops(self, strict=False): + def has_loops(self, *, strict=False): """Retorna True si la matriz contiene loops de dominacia. A loop is defined as if there are alternatives `a0`, `a1` and 'a2' such From e4933c2a99d39e2b772861fb6babe59e23433b6f Mon Sep 17 00:00:00 2001 From: JuanBC Date: Mon, 21 Feb 2022 15:39:36 -0300 Subject: [PATCH 47/47] fix 3.10 to test --- .github/workflows/CI.yml | 2 +- docs/source/tutorial/sufdom.ipynb | 107 ++++++++++++++++-------------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5450912..4d81b65 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,4 +13,4 @@ env: jobs: testing: - uses: quatrope/scikit-criteria/.github/workflows/tests.yml@991bafd28ebc638429e2fcd2c69d7cb9722209ea \ No newline at end of file + uses: quatrope/scikit-criteria/.github/workflows/tests.yml@0cd9099455b6e45659347dc08e6eeec95f876c10 \ No newline at end of file diff --git a/docs/source/tutorial/sufdom.ipynb b/docs/source/tutorial/sufdom.ipynb index 0634734..14564b6 100644 --- a/docs/source/tutorial/sufdom.ipynb +++ b/docs/source/tutorial/sufdom.ipynb @@ -192,16 +192,16 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Filter(criteria_filters={'ROE': }, ignore_missing_criteria=False)" + "Filter(criteria_filters={'ROE': }, ignore_missing_criteria=False)" ] }, - "execution_count": 52, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -230,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -239,7 +239,7 @@ "FilterGE(criteria_filters={'ROE': 2}, ignore_missing_criteria=False)" ] }, - "execution_count": 53, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -259,7 +259,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -335,7 +335,7 @@ "[5 Alternatives x 3 Criteria]" ] }, - "execution_count": 54, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -359,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -421,7 +421,7 @@ "[3 Alternatives x 3 Criteria]" ] }, - "execution_count": 55, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -537,7 +537,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -551,7 +551,7 @@ "dtype: bool" ] }, - "execution_count": 10, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -571,7 +571,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -585,7 +585,7 @@ "dtype: bool" ] }, - "execution_count": 13, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -609,7 +609,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -618,7 +618,7 @@ "array(['PE', 'AA', 'FN'], dtype=object)" ] }, - "execution_count": 24, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -637,7 +637,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -722,7 +722,7 @@ "FN False False False True False" ] }, - "execution_count": 26, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -746,7 +746,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -755,7 +755,7 @@ "" ] }, - "execution_count": 56, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, @@ -787,7 +787,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -1094,13 +1094,13 @@ "source": [ "### Filter non-dominated alternatives\n", "\n", - "Finalmente skcriteria ofrece una forma de filtar alternativas non-dominadas,\n", - "la cual acepta como parametro si quiere evaluar la dominancia estrictca." + "Finally skcriteria offers a way to filter non-dominated alternatives,\n", + "which it accepts as a parameter if you want to evaluate strict dominance." ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -1109,7 +1109,7 @@ "FilterNonDominated(strict=True)" ] }, - "execution_count": 43, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -1121,7 +1121,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -1190,7 +1190,7 @@ "[4 Alternatives x 3 Criteria]" ] }, - "execution_count": 44, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -1205,24 +1205,24 @@ "source": [ "## Full expermient\n", "\n", - "Podemos finalmente crear un experimento completo de MCDA que tome en cuenta\n", - "las en análisis de satisfaccion y dominancia.\n", + "We can finally create a complete MCDA experiment that takes into account\n", + "the in satisfaction and dominance analysis.\n", "\n", - "El expeimento completo tendria las siguientes etapas\n", + "The complete experiment would have the following steps\n", "\n", - "1. Se eliminan alternativas que no que rindan al menos el 2% ($ROE >= 2$).\n", - "2. Se eliminan alternativas dominadas.\n", - "3. Se convierten todos los criterios a maximizar.\n", - "4. Se excala los pesos por la suma todal.\n", - "5. Se escala la matriz por el modulo del vecto.\n", - "6. Se aplica [TOPSIS](https://en.wikipedia.org/wiki/TOPSIS).\n", + "1. Eliminate alternatives that do not yield at least 2% ($ROE >= $2).\n", + "2. Eliminate dominated alternatives.\n", + "3. Convert all criteria to maximize.\n", + "4. The weights are scaled by the total sum.\n", + "5. The matrix is scaled by the vector modulus.\n", + "6. Apply [TOPSIS](https://en.wikipedia.org/wiki/TOPSIS).\n", "\n", - "Para esto lo mas conveniente es utilizar un pipeline" + "The most convenient way to do this is to use a pipeline." ] }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -1231,7 +1231,7 @@ "SKCPipeline(steps=[('filterge', FilterGE(criteria_filters={'ROE': 2}, ignore_missing_criteria=False)), ('filternondominated', FilterNonDominated(strict=True)), ('minimizetomaximize', MinimizeToMaximize()), ('sumscaler', SumScaler(target='weights')), ('vectorscaler', VectorScaler(target='matrix')), ('topsis', TOPSIS(metric='euclidean'))])" ] }, - "execution_count": 49, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -1257,12 +1257,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Ahora aplicamos el pipeline a los datos originales" + "We now apply the pipeline to the original data" ] }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -1271,7 +1271,7 @@ "
\n", "\n", - "\n", + "
\n", " \n", " \n", " \n", @@ -1283,11 +1283,11 @@ " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", "
 
Rank3421Rank3421
\n", @@ -1300,7 +1300,7 @@ "[Method: TOPSIS]" ] }, - "execution_count": 51, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -1318,9 +1318,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Scikit-Criteria version: 0.6dev0\n", + "Running datetime: 2022-02-21 15:21:22.346578\n" + ] + } + ], "source": [ "import datetime as dt\n", "import skcriteria\n",