Basic example: choose a car

First we configure the notebook so we can plot figures (first line), and also we have autocompletion.

[1]:
%matplotlib inline
%config Completer.use_jedi = False

Problem formalization

The beginning of all problem resolution is to formalize the problem. In Multi-Criteria Decision Analysis, one such problem can consist of choosing one alternative among a set.

[2]:
alternatives = ["Fiat 500", "Peugeot 309", "Renault Clio", "Opel Astra", "Honda Civic", "Toyota Corolla"]

We can list criteria that are considered relevant to make that choice:

[3]:
criteria = ["cost", "fuel consumption", "comfort", "color", "range"]

Those criteria can have different types of scales associated to them and are in general heterogeneous: * The cost in euros is to be minimized * The fuel consumption in L/100km is to be minimized * The comfort can be rated with stars, and must be maximized * The color is way more subjective and can be associated to a ranking (here the decider prefers red cars and hates grey ones) * The range is in km and must be maximized

We must therefore enter the limits of those scales for the current MCDA problem and associate non-numeric criteria values to numeric values if we want to make computations later on.

[4]:
from mcda import PerformanceTable
from mcda.scales import *
[5]:
scale1 = QuantitativeScale(6000, 20000, preference_direction=MIN)
scale2 = QuantitativeScale(4, 6, preference_direction=MIN)
scale3 = QualitativeScale({"*": 1, "**": 2, "***": 3, "****": 4})
scale4 = QualitativeScale(
    {"red": 1, "blue": 2, "black": 3, "grey": 4},
    preference_direction=MIN
)
scale5 = QuantitativeScale(400, 1000)
scales = {
    criteria[0]: scale1,
    criteria[1]: scale2,
    criteria[2]: scale3,
    criteria[3]: scale4,
    criteria[4]: scale5
}

Then we can gather the data for each alternative and criterion in a performance table:

[6]:
performance_table = PerformanceTable(
    [
        [9500, 4.2, "**", "blue", 450],
        [15600, 4.5, "****", "black", 900],
        [6800, 4.1, "***", "grey", 700],
        [10200, 5.6, "****", "black", 850],
        [8100, 5.2, "***", "red", 750],
        [12000, 4.9, "****", "grey", 850]
    ],
    alternatives=alternatives,
    criteria=criteria,
    scales=scales,
)
performance_table.data
[6]:
cost fuel consumption comfort color range
Fiat 500 9500 4.2 ** blue 450
Peugeot 309 15600 4.5 **** black 900
Renault Clio 6800 4.1 *** grey 700
Opel Astra 10200 5.6 **** black 850
Honda Civic 8100 5.2 *** red 750
Toyota Corolla 12000 4.9 **** grey 850

To orient the decision process, it is important to visualize the data on which we will base the decision: the performance table

[7]:
from mcda.plot import *
[8]:
fig = Figure(ncols=2, figsize=(6.4, 9.6))

x = [*range(len(alternatives))]
xticks = x
xticklabels = alternatives
for i in criteria:
    values = performance_table.criteria_values[i].data
    ax = fig.create_add_axis()
    ax.title = i
    ax.xlabel = "alternatives"
    ax.ylabel = "performances"
    yticks = None
    yticklabels = None
    y = values
    if isinstance(scales[i], QualitativeScale):
        y = [scales[i].value(v) for v in values]
        yticklabels = scales[i].range()
        yticks = [
            scales[i].value(yy) for yy in yticklabels
        ]
    elif isinstance(scales[i], NominalScale):
        yticklabels = scales[i].range()
        yticks = [*range(len(yticklabels))]
        y = [yticks[yticklabels.index(v)] for v in values]
    ax.add_plot(
        BarPlot(
            x,
            y,
            xticks=xticks,
            yticks=yticks,
            xticklabels=xticklabels,
            yticklabels=yticklabels,
            xticklabels_tilted=True
        )
    )
fig.draw()
/home/nduminy/pymcda/src/mcda/internal/plot/plot.py:296: UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure.
  self.fig.show()
../../_images/notebooks_tutorials_car_example_13_1.png

It is quite difficult to take a decision solely based on this graph, we can therefore start a multistep decision process.

Decision process

We can easily check that not all performances from our performance table are numeric, which makes them hard to compare:

[9]:
performance_table.is_numeric
[9]:
False

To compare those performances, we can transform them all to numeric values. We could use the mcda.transformers module for that, however a simple conversion to numeric values is direcly possible through the PerformanceTable to_numeric property:

[10]:
performance_table.to_numeric.data
[10]:
cost fuel consumption comfort color range
Fiat 500 9500 4.2 2 2 450
Peugeot 309 15600 4.5 4 3 900
Renault Clio 6800 4.1 3 4 700
Opel Astra 10200 5.6 4 3 850
Honda Civic 8100 5.2 3 1 750
Toyota Corolla 12000 4.9 4 4 850

However the ranges of those numerical values still varies wildly. Furthermore their preference direction may also differ. In other words, we need to normalize them.

The scales contain all information necessary for this operation, they contain also the preferrence order of each criterion (ex: cost must be minimized, range maximized, etc). This way we can also perform the normalization so that normalized criteria values are all in increasing order:

[11]:
from mcda import normalize
[12]:
normalized_table = normalize(performance_table)
normalized_table.data
[12]:
cost fuel consumption comfort color range
Fiat 500 0.750000 0.90 0.333333 0.666667 0.083333
Peugeot 309 0.314286 0.75 1.000000 0.333333 0.833333
Renault Clio 0.942857 0.95 0.666667 0.000000 0.500000
Opel Astra 0.700000 0.20 1.000000 0.333333 0.750000
Honda Civic 0.850000 0.40 0.666667 1.000000 0.583333
Toyota Corolla 0.571429 0.55 1.000000 0.000000 0.750000

Radar or spider plot are then a very good way to visualize all the normalized values per alternative and criterion:

[13]:
create_radar_projection(len(alternatives), frame='polygon')

fig = Figure(ncols=2, figsize=(6, 10))
for i in criteria:
    values = normalized_table.criteria_values[i].data
    ax = fig.create_add_axis(
        projection=radar_projection_name(len(alternatives)))
    ax.title = i
    ax.add_plot(
        RadarPlot(
            alternatives,
            values
        )
    )
fig.draw()
/home/nduminy/pymcda/src/mcda/internal/plot/plot.py:296: UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure.
  self.fig.show()
../../_images/notebooks_tutorials_car_example_22_1.png

Sum as alternatives scores

If the user has no priority between the criteria, we can simply compute the score of each alternative by summing all its criteria values:

[14]:
alternatives_values = normalized_table.sum(axis=1)
alternatives_values.data
[14]:
Fiat 500          2.733333
Peugeot 309       3.230952
Renault Clio      3.059524
Opel Astra        2.983333
Honda Civic       3.500000
Toyota Corolla    2.871429
dtype: float64

We have a winner! Or at least a ranking that will inform the user for its choice.

[15]:
x = [*range(len(alternatives))]
plot = BarPlot(
    x, alternatives_values.data, xticks=x,
    xticklabels=alternatives, xticklabels_tilted=True
)
plot.draw()
plot.axis.title = "alternatives score"
plot.axis.xlabel = "alternatives"
plot.axis.ylabel = "score"
plot.axis.figure.draw()
../../_images/notebooks_tutorials_car_example_26_0.png

However, our user can also assign a different importance to each criterion.

Weighted sum as alternatives scores

The simplest way to formalize different criteria importance, it to define a weight for each criterion.

Our user is really interested by range of its future car, comfort is also important, but the cost is not an issue:

[16]:
from mcda.mavt.aggregators import WeightedSum

weighted_sum = WeightedSum({
    criteria[0]: 1,
    criteria[1]: 2,
    criteria[2]: 3,
    criteria[3]: 2,
    criteria[4]: 4
})

We can then multiply each normalized criterion value in the performance table by the corresponding criterion weight. We get the scores per alternative and criterion:

[17]:
weighted_table = weighted_sum.weight_functions(normalized_table)
weighted_table.data
[17]:
cost fuel consumption comfort color range
Fiat 500 0.750000 1.8 1.0 1.333333 0.333333
Peugeot 309 0.314286 1.5 3.0 0.666667 3.333333
Renault Clio 0.942857 1.9 2.0 0.000000 2.000000
Opel Astra 0.700000 0.4 3.0 0.666667 3.000000
Honda Civic 0.850000 0.8 2.0 2.000000 2.333333
Toyota Corolla 0.571429 1.1 3.0 0.000000 3.000000

We can visualize those scores, again using for example a spider plot:

[18]:
create_radar_projection(len(criteria), frame='polygon')

fig = Figure(ncols=2, figsize=(6, 10))
for i in alternatives:
    values = weighted_table.alternatives_values[i].data
    ax = fig.create_add_axis(
        projection=radar_projection_name(len(criteria)))
    ax.title = i
    ax.add_plot(
        RadarPlot(
            criteria,
            values
        )
    )
fig.draw()
/home/nduminy/pymcda/src/mcda/internal/plot/plot.py:296: UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure.
  self.fig.show()
../../_images/notebooks_tutorials_car_example_32_1.png

If we compute the alternatives global scores, we get a different ranking:

[19]:
alternatives_values = weighted_table.sum(axis=1)
alternatives_values.data
[19]:
Fiat 500          5.216667
Peugeot 309       8.814286
Renault Clio      6.842857
Opel Astra        7.766667
Honda Civic       7.983333
Toyota Corolla    7.671429
dtype: float64
[20]:
x = [*range(len(alternatives))]
plot = BarPlot(
    x, alternatives_values.data, xticks=x,
    xticklabels=alternatives, xticklabels_tilted=True
)
plot.draw()
plot.axis.title = "alternatives score"
plot.axis.xlabel = "alternatives"
plot.axis.ylabel = "score"
plot.axis.figure.draw()
../../_images/notebooks_tutorials_car_example_35_0.png