Multi Attribute Value Theory

This notebook is dedicated to the use of MAVT algorithms, including aggregators like the weighted sum, but also the disaggregators of the UTA algorithmic family.

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

MAVT problem formalization

We first need to define our decision problem within the MAVT framework: * Alternatives * Criteria * Performance table * Criteria scales

The following uses the car choice example from:

  1. Jacquet-Lagrèze and Y. Siskos. Assessing a set of additive utility functions for multicriteria decision making: The UTA method. European Journal of Operational Research, 10(2): 151–164, 1982

[2]:
from mcda import PerformanceTable
from mcda.scales import *
from mcda.relations import *
[3]:
alternatives = [
    "Peugeot 505 GR",
    "Opel Record 2000 LS",
    "Citroen Visa Super E",
    "VW Golf 1300 GLS",
    "Citroen CX 2400 Pallas",
    "Mercedes 230",
    "BMW 520",
    "Volvo 244 DL",
    "Peugeot 104 ZS",
    "Citroen Dyane"
]
[4]:
criteria = [
    "MaximalSpeed",
    "ConsumptionTown",
    "Consumption120kmh",
    "HP",
    "Space",
    "Price"
]
[5]:
scales = {
    criteria[0]: QuantitativeScale(110, 190),
    criteria[1]: QuantitativeScale(7, 15, preference_direction=MIN),
    criteria[2]: QuantitativeScale(6, 13, preference_direction=MIN),
    criteria[3]: QuantitativeScale(3, 13),
    criteria[4]: QuantitativeScale(5, 9),
    criteria[5]: QuantitativeScale(20000, 80000, preference_direction=MIN)
}
[6]:
performance_table = PerformanceTable(
    [
        [173, 11.4, 10.01, 10, 7.88, 49500],
        [176, 12.3, 10.48, 11, 7.96, 46700],
        [142, 8.2, 7.3, 5, 5.65, 32100],
        [148, 10.5, 9.61, 7, 6.15, 39150],
        [178, 14.5, 11.05, 13, 8.06, 64700],
        [180, 13.6, 10.4, 13, 8.47, 75700],
        [182, 12.7, 12.26, 11, 7.81, 68593],
        [145, 14.3, 12.95, 11, 8.38, 55000],
        [161, 8.6, 8.42, 7, 5.11, 35200],
        [117, 7.2, 6.75, 3, 5.81, 24800]
    ],
    alternatives=alternatives,
    criteria=criteria,
    scales=scales
)

Aggregators

For all aggregators, we can define their input and output scales. This is purely optional, they are inferred from data otherwise. Input data is neither checked nor modified to belong in the input scales, they are purely informative.

Weighted sum

[7]:
from mcda.mavt.aggregators import WeightedSum
from mcda import normalize

Which is perhaps the easiest and simplest MCDA method requires setting the user preferences using a set of criterion weights:

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

Then all is set to apply the weighted sum.

[9]:
alternatives_grades = weighted_sum(normalize(performance_table))
alternatives_grades.data
[9]:
Peugeot 505 GR             9.505119
Opel Record 2000 LS        9.212500
Citroen Visa Super E      10.321905
VW Golf 1300 GLS           8.529405
Citroen CX 2400 Pallas     6.799643
Mercedes 230               7.249524
BMW 520                    6.919395
Volvo 244 DL               6.735952
Peugeot 104 ZS             9.442738
Citroen Dyane             11.238214
dtype: float64

We have obtained the alternatives’ scores, according to the criteria weights chosen.

We can now sort the alternatives according to these scores.

[10]:
alternatives_grades.sort().data
[10]:
Citroen Dyane             11.238214
Citroen Visa Super E      10.321905
Peugeot 505 GR             9.505119
Peugeot 104 ZS             9.442738
Opel Record 2000 LS        9.212500
VW Golf 1300 GLS           8.529405
Mercedes 230               7.249524
BMW 520                    6.919395
Citroen CX 2400 Pallas     6.799643
Volvo 244 DL               6.735952
dtype: float64

Choquet integral

[11]:
from mcda.set_functions import SetFunction
from mcda.mavt.aggregators import ChoquetIntegral

We need to define the capacity related to the set of criteria of the problem. This version of the package has not yet implemented a way to elicit a capacity. Hence the following test will be achieved with the uniform capacity:

[12]:
capacity = SetFunction.uniform_capacity(criteria)

Now we can compute the aggregated score of each alternative using the Choquet integral on the normalized performance table with the uniform capacity.

[13]:
choquet_integral_capacity = ChoquetIntegral(capacity)
alternatives_grades = choquet_integral_capacity(normalize(performance_table))
alternatives_grades.data
[13]:
Peugeot 505 GR            0.598829
Opel Record 2000 LS       0.602917
Citroen Visa Super E      0.537520
VW Golf 1300 GLS          0.481687
Citroen CX 2400 Pallas    0.535179
Mercedes 230              0.560099
BMW 520                   0.497638
Volvo 244 DL              0.432302
Peugeot 104 ZS            0.544325
Citroen Dyane             0.512976
dtype: float64

N.B: Choquet integral can also be computed using a capacity Möbius representation

[14]:
choquet_integral_mobius = ChoquetIntegral(capacity.mobius)
alternatives_grades = choquet_integral_mobius(normalize(performance_table))
alternatives_grades.data
[14]:
Peugeot 505 GR            0.598829
Opel Record 2000 LS       0.602917
Citroen Visa Super E      0.537520
VW Golf 1300 GLS          0.481687
Citroen CX 2400 Pallas    0.535179
Mercedes 230              0.560099
BMW 520                   0.497638
Volvo 244 DL              0.432302
Peugeot 104 ZS            0.544325
Citroen Dyane             0.512976
dtype: float64

We have obtained the alternatives’ scores, according to the capacity chosen and the Choquet integral as an aggregator.

We can now sort the alternatives according to these scores.

[15]:
alternatives_grades.sort().data
[15]:
Opel Record 2000 LS       0.602917
Peugeot 505 GR            0.598829
Mercedes 230              0.560099
Peugeot 104 ZS            0.544325
Citroen Visa Super E      0.537520
Citroen CX 2400 Pallas    0.535179
Citroen Dyane             0.512976
BMW 520                   0.497638
VW Golf 1300 GLS          0.481687
Volvo 244 DL              0.432302
dtype: float64

OWA

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

We need to define OWA weights. Here is an example with the extreme and weights:

[17]:
owa = OWA.and_aggregator(len(criteria))

We can use OWA to aggregates normalized performances into alternatives’ grades:

[18]:
alternatives_grades = owa(normalize(performance_table))
alternatives_grades.data
[18]:
Peugeot 505 GR            0.427143
Opel Record 2000 LS       0.337500
Citroen Visa Super E      0.162500
VW Golf 1300 GLS          0.287500
Citroen CX 2400 Pallas    0.062500
Mercedes 230              0.071667
BMW 520                   0.105714
Volvo 244 DL              0.007143
Peugeot 104 ZS            0.027500
Citroen Dyane             0.000000
dtype: float64

We have obtained the alternatives’ scores, according to the capacity chosen and the Choquet integral as an aggregator.

We can now sort the alternatives according to these scores.

[19]:
alternatives_grades.sort().data
[19]:
Peugeot 505 GR            0.427143
Opel Record 2000 LS       0.337500
VW Golf 1300 GLS          0.287500
Citroen Visa Super E      0.162500
BMW 520                   0.105714
Mercedes 230              0.071667
Citroen CX 2400 Pallas    0.062500
Peugeot 104 ZS            0.027500
Volvo 244 DL              0.007143
Citroen Dyane             0.000000
dtype: float64

ULOWA

[20]:
from pandas import Series
from mcda.functions import FuzzyNumber
from mcda.scales import FuzzyScale
from mcda.mavt.aggregators import ULOWA

We need to define a ULOWA problem, as well as the weights used during the aggregation. For this we define a fuzzy partition using fuzzy numbers, which we associate to labels inside a fuzzy scale:

[21]:
fuzzy_sets = [
    FuzzyNumber([0.0, 0.0, 0.0, 2.0]),
    FuzzyNumber([0.0, 2.0, 2.0, 5.0]),
    FuzzyNumber([2.0, 5.0, 5.0, 6.0]),
    FuzzyNumber([5.0, 6.0, 6.0, 7.0]),
    FuzzyNumber([6.0, 7.0, 8.0, 9.0]),
    FuzzyNumber([8.0, 9.0, 9.0, 10.0]),
    FuzzyNumber([9.0, 10.0, 10.0, 10.0])
]
labels = ["VL", "L", "M", "AH", "H", "VH", "P"]
uscale = FuzzyScale(Series(fuzzy_sets, index=labels))

uweights = [0.0, 0.0, 0.5, 0.5, 0.0]

ualternatives = ["Caleta", "Tarakon", "Dominos", "Ancora", "Frida", "Cucafera"]
ucriteria = ["Food", "Service", "Atmosphere", "Category", "Location"]

uperformance_table = PerformanceTable(
    [
        ["VL", "VL", "P", "H", "VL"],
        ["VL", "VL", "H", "P", "P"],
        ["VL", "VL", "L", "M", "L"],
        ["VH", "L", "H", "H", "AH"],
        ["P", "L", "H", "L", "AH"],
        ["P", "VL", "VL", "M", "AH"]
    ],
    alternatives=ualternatives,
    criteria=ucriteria
)

ulowa = ULOWA(uweights, uscale)

As ULOWA has been designed for fuzzy partition, we can check that our fuzzy scale does define such partition:

[22]:
uscale.is_fuzzy_partition
[22]:
True

Now, we can compute the ULOWA result for each alternative.

[23]:
results1 = ulowa(uperformance_table)
results1.data
[23]:
Caleta      VL
Tarakon      M
Dominos     VL
Ancora      AH
Frida        M
Cucafera     L
dtype: object

Note that this corresponds to a ranking, as those values can be ordered:

[24]:
results1.sort().data
[24]:
Ancora      AH
Frida        M
Tarakon      M
Cucafera     L
Dominos     VL
Caleta      VL
dtype: object

We can also compute some measures to characterize the fuzzy numbers w.r.t to the boundaries of the scale.

[25]:
data = [uscale.fuzziness(fz) for fz in fuzzy_sets]
print(data)
[0.1, 0.25, 0.2, 0.1, 0.1, 0.1, 0.05]
[26]:
data = [uscale.specificity(fz) for fz in fuzzy_sets]
print(data)
[0.9, 0.75, 0.8, 0.9, 0.8, 0.9, 0.95]

Now let’s change the weights and recompute ULOWA.

[27]:
uweights = [0.2, 0.6, 0.2, 0.0, 0.0]
ulowa2 = ULOWA(uweights, uscale)
results2 = ulowa2(uperformance_table)
results2.data
[27]:
Caleta      AH
Tarakon     VH
Dominos      L
Ancora       H
Frida        H
Cucafera    AH
dtype: object

TOPSIS

TOPSIS is an aggregation technique which compares weighted normalized alternative values to fictive alternatives called positive (respectively negative) solution corresponding to the best (resp. worst) criterion values taken from the input performance table.

[28]:
from mcda.mavt.aggregators import TOPSIS

The only argument needed to setup TOPSIS is the criteria weights.

[29]:
criteria_weights = {
    criteria[0]: 1,
    criteria[1]: 5,
    criteria[2]: 2,
    criteria[3]: 1,
    criteria[4]: 4,
    criteria[5]: 4
}
topsis = TOPSIS(criteria_weights)

Note: the weights are internally normalized before application to the table of normalized values.

[30]:
topsis.normalized_weights
[30]:
{'MaximalSpeed': 0.058823529411764705,
 'ConsumptionTown': 0.29411764705882354,
 'Consumption120kmh': 0.11764705882352941,
 'HP': 0.058823529411764705,
 'Space': 0.23529411764705882,
 'Price': 0.23529411764705882}

Then you can apply TOPSIS aggregation to the performance table, and check its resulting scores (higher is better):

[31]:
result = topsis(performance_table).sort()
print(result.data)
Citroen Dyane             0.738436
Citroen Visa Super E      0.700240
Peugeot 104 ZS            0.651177
VW Golf 1300 GLS          0.592510
Peugeot 505 GR            0.524999
Opel Record 2000 LS       0.515510
Volvo 244 DL              0.387210
Citroen CX 2400 Pallas    0.326306
BMW 520                   0.317553
Mercedes 230              0.316707
dtype: float64

There are various methods used inside the TOPSIS algorithm. here are some examples:

TOPSIS normalization method:

[32]:
TOPSIS.normalize(performance_table).data
[32]:
MaximalSpeed ConsumptionTown Consumption120kmh HP Space Price
Peugeot 505 GR 0.338750 0.310784 0.313363 0.327385 0.344570 0.302964
Opel Record 2000 LS 0.344624 0.335319 0.328077 0.360124 0.348068 0.285826
Citroen Visa Super E 0.278049 0.223546 0.228527 0.163693 0.247059 0.196467
VW Golf 1300 GLS 0.289797 0.286248 0.300841 0.229170 0.268922 0.239617
Citroen CX 2400 Pallas 0.348540 0.395295 0.345920 0.425601 0.352441 0.395995
Mercedes 230 0.352456 0.370759 0.325572 0.425601 0.370369 0.463320
BMW 520 0.356373 0.346224 0.383799 0.360124 0.341509 0.419822
Volvo 244 DL 0.283923 0.389843 0.405400 0.360124 0.366434 0.336626
Peugeot 104 ZS 0.315253 0.234451 0.263588 0.229170 0.223446 0.215441
Citroen Dyane 0.229097 0.196284 0.211309 0.098216 0.254055 0.151788

positive and negative ideal solutions used by TOPSIS:

[33]:
TOPSIS.positive_ideal_solution(performance_table).data
[33]:
MaximalSpeed           182.00
ConsumptionTown          7.20
Consumption120kmh        6.75
HP                      13.00
Space                    8.47
Price                24800.00
dtype: float64
[34]:
TOPSIS.negative_ideal_solution(performance_table).data
[34]:
MaximalSpeed           117.00
ConsumptionTown         14.50
Consumption120kmh       12.95
HP                       3.00
Space                    5.11
Price                75700.00
dtype: float64

Disaggregators

UTA

[35]:
from mcda.mavt.uta import UTA
from mcda.relations import PreferenceRelation, PreferenceStructure

This method infers the user model of preferences as marginal utility functions, which can then be used to score each alternatives’ criterion performance. We can infer the global score of each alternative by summing those marginal utility values.

Those marginal utility functions are piecewise linear functions.

The algorithm requires the following: * The number of segments to use for each marginal utility function (per criterion) * The pair-wise preference/indifference relations representing the user preferences

[36]:
criteria_segments = {
    criteria[0]: 5,
    criteria[1]: 4,
    criteria[2]: 4,
    criteria[3]: 5,
    criteria[4]: 4,
    criteria[5]: 5
}
[37]:
relations = PreferenceStructure()
for i in range(len(alternatives)-1):
    relations += P(alternatives[i], alternatives[i+1])
# Meaning alternatives are already ordered by preference

We have everything we need to apply UTA:

[38]:
uta = UTA(performance_table, relations, criteria_segments, delta=0.01)
# 'delta' is the preference threshold
value_functions = uta.disaggregate()
Welcome to the CBC MILP Solver
Version: 2.10.3
Build Date: Dec 15 2019

command line - /home/nduminy/Development/pymcda/.venv/lib/python3.10/site-packages/pulp/apis/../solverdir/cbc/linux/64/cbc /tmp/e5b2d5248e714613acbbfbd174e74817-pulp.mps timeMode elapsed branch printingOptions all solution /tmp/e5b2d5248e714613acbbfbd174e74817-pulp.sol (default strategy 1)
At line 2 NAME          MODEL
At line 3 ROWS
At line 48 COLUMNS
At line 305 RHS
At line 349 BOUNDS
At line 350 ENDATA
Problem MODEL has 43 rows, 43 columns and 231 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Presolve 28 (-15) rows, 33 (-10) columns and 188 (-43) elements
Perturbing problem by 0.001% of 0.93732919 - largest nonzero change 4.9244589e-05 ( 0.0083799674%) - largest zero change 4.8475482e-05
0  Obj 0 Primal inf 3.2519376 (9)
22  Obj 5.7170759e-05
Optimal - objective value 0
After Postsolve, objective 0, infeasibilities - dual 0 (0), primal 0 (0)
Optimal objective 0 - 22 iterations time 0.002, Presolve 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.00   (Wallclock seconds):       0.00

We can plot those marginal value functions for a better understanding of UTA’s results.

[39]:
import mcda.plot as pplot
[40]:
fig = pplot.Figure(ncols=2)
for i in criteria:
    x = value_functions.in_scales[i].range(500)
    y = [value_functions.functions[i](xx) for xx in x]
    ax = fig.create_add_axis()
    ax.title = i
    ax.add_plot(pplot.LinePlot(x, y))
fig.draw()

We can apply those marginal value functions as criteria functions to the performance table, to obtain the table of marginal scores.

[41]:
uta_values = value_functions(performance_table)
uta_values.data
[41]:
MaximalSpeed ConsumptionTown Consumption120kmh HP Space Price
Peugeot 505 GR 0.368427 0.021762 0.013051 -0.000000 0.064203 0.037012
Opel Record 2000 LS 0.370797 0.009521 0.012922 -0.000000 0.064203 0.037012
Citroen Visa Super E 0.332877 0.027202 0.045632 -0.000000 0.041732 0.037012
VW Golf 1300 GLS 0.332877 0.027202 0.013161 -0.000000 0.064203 0.037012
Citroen CX 2400 Pallas 0.370797 -0.000000 0.012766 0.006511 0.064203 0.010178
Mercedes 230 0.370797 -0.000000 0.012944 0.006511 0.064203 -0.000000
BMW 520 0.370797 0.004080 0.005375 -0.000000 0.064203 -0.000000
Volvo 244 DL 0.332877 -0.000000 0.000363 -0.000000 0.064203 0.037012
Peugeot 104 ZS 0.339987 0.027202 0.013191 -0.000000 0.007062 0.037012
Citroen Dyane -0.000000 0.027202 0.085281 -0.000000 0.052004 0.249967

We can apply the aggregator returned by UTA to obtain alternatives scores. You can see that in our case, UTA keeps the same order of alternatives.

[42]:
alternatives_grades = uta_values.sum(1)
alternatives_grades.data
[42]:
Peugeot 505 GR            0.504455
Opel Record 2000 LS       0.494455
Citroen Visa Super E      0.484455
VW Golf 1300 GLS          0.474455
Citroen CX 2400 Pallas    0.464455
Mercedes 230              0.454455
BMW 520                   0.444455
Volvo 244 DL              0.434455
Peugeot 104 ZS            0.424455
Citroen Dyane             0.414455
dtype: float64

We can now sort the alternatives according to these scores.

[43]:
alternatives_grades.sort().data
[43]:
Peugeot 505 GR            0.504455
Opel Record 2000 LS       0.494455
Citroen Visa Super E      0.484455
VW Golf 1300 GLS          0.474455
Citroen CX 2400 Pallas    0.464455
Mercedes 230              0.454455
BMW 520                   0.444455
Volvo 244 DL              0.434455
Peugeot 104 ZS            0.424455
Citroen Dyane             0.414455
dtype: float64

You can look at the base LP problem using the attribute problem, and also directly look at the objective value:

[44]:
uta.objective
[44]:
0.0

N.B: if using post-optimality, the property objective will refer to the post-optimal solution objective value. You can still view the base problem objective va

UTA*

[45]:
from mcda.mavt.uta import UTAstar
[46]:
uta_star = UTAstar(performance_table, relations, criteria_segments, delta=0.01)
# 'delta' is the preference threshold
value_functions = uta_star.disaggregate()
Welcome to the CBC MILP Solver
Version: 2.10.3
Build Date: Dec 15 2019

command line - /home/nduminy/Development/pymcda/.venv/lib/python3.10/site-packages/pulp/apis/../solverdir/cbc/linux/64/cbc /tmp/75a8f2f571354b3c88ae6ec06672fe91-pulp.mps timeMode elapsed branch printingOptions all solution /tmp/75a8f2f571354b3c88ae6ec06672fe91-pulp.sol (default strategy 1)
At line 2 NAME          MODEL
At line 3 ROWS
At line 15 COLUMNS
At line 344 RHS
At line 355 BOUNDS
At line 356 ENDATA
Problem MODEL has 10 rows, 53 columns and 174 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Presolve 10 (0) rows, 44 (-9) columns and 164 (-10) elements
Perturbing problem by 0.001% of 4.4984539 - largest nonzero change 6.8916459e-05 ( 0.0015320032%) - largest zero change 6.8191646e-05
0  Obj 0 Primal inf 0.089991 (9)
13  Obj 3.8786125e-06
Optimal - objective value 0
After Postsolve, objective 0, infeasibilities - dual 0 (0), primal 0 (0)
Optimal objective 0 - 12 iterations time 0.002, Presolve 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.00   (Wallclock seconds):       0.00

[40]:
alternatives_grades = value_functions.aggregate(value_functions(performance_table))
alternatives_grades.data
[40]:
Peugeot 505 GR            0.951774
Opel Record 2000 LS       0.941774
Citroen Visa Super E      0.931774
VW Golf 1300 GLS          0.921774
Citroen CX 2400 Pallas    0.911774
Mercedes 230              0.901774
BMW 520                   0.891774
Volvo 244 DL              0.881774
Peugeot 104 ZS            0.871774
Citroen Dyane             0.861774
dtype: float64

AHP

[41]:
from mcda.matrices import AdjacencyValueMatrix
from mcda.mavt.aggregators import AHP, PartialAHP

Partial Problem

[42]:
drink_consumption = AdjacencyValueMatrix(
    [
        [1, 9, 5, 2, 1, 1, 1/2],
        [1/9, 1, 1/3, 1/9, 1/9, 1/9, 1/9],
        [1/5, 2, 1, 1/3, 1/4, 1/3, 1/9],
        [1/2, 9, 3, 1, 1/2, 1, 1/3],
        [1, 9, 4, 2, 1, 2, 1/2],
        [1, 9, 3, 1, 1/2, 1, 1/3],
        [2, 9, 9, 3, 2, 3, 1]
    ],
    vertices=["Coffee", "Wine", "Tea", "Beer", "Soda", "Milk", "Water"]
)
drink_consumption.data
[42]:
Coffee Wine Tea Beer Soda Milk Water
Coffee 1.000000 9 5.000000 2.000000 1.000000 1.000000 0.500000
Wine 0.111111 1 0.333333 0.111111 0.111111 0.111111 0.111111
Tea 0.200000 2 1.000000 0.333333 0.250000 0.333333 0.111111
Beer 0.500000 9 3.000000 1.000000 0.500000 1.000000 0.333333
Soda 1.000000 9 4.000000 2.000000 1.000000 2.000000 0.500000
Milk 1.000000 9 3.000000 1.000000 0.500000 1.000000 0.333333
Water 2.000000 9 9.000000 3.000000 2.000000 3.000000 1.000000
[43]:
drink_ahp = PartialAHP(drink_consumption)
[44]:
drink_scores = drink_ahp.to_values()
drink_scores.data
[44]:
Coffee    0.177746
Wine      0.019256
Tea       0.039269
Beer      0.116738
Soda      0.190395
Milk      0.129234
Water     0.327361
dtype: float64
[45]:
drink_ahp.consistency_ratio()
[45]:
0.014177307088299076

We can compute, as a comparison, the ideal reciproqual pairwise matrix represented by the partial AHP values (or priority vectors):

[46]:
ideal_pairwise_table = AdjacencyValueMatrix([
    [
            value1.value / value2.value
            for a2, value2 in drink_scores.items()
        ]
        for a1, value1 in drink_scores.items()
    ],
    vertices=drink_scores.labels
)
ideal_pairwise_table.data
[46]:
Coffee Wine Tea Beer Soda Milk Water
Coffee 1.000000 9.230703 4.526359 1.522607 0.933564 1.375385 0.542967
Wine 0.108334 1.000000 0.490359 0.164950 0.101137 0.149001 0.058822
Tea 0.220928 2.039322 1.000000 0.336387 0.206250 0.303861 0.119957
Beer 0.656768 6.062432 2.972769 1.000000 0.613135 0.903309 0.356603
Soda 1.071164 9.887600 4.848474 1.630963 1.000000 1.473263 0.581606
Milk 0.727069 6.711359 3.290976 1.107041 0.678765 1.000000 0.394774
Water 1.841734 17.000498 8.336348 2.804237 1.719376 2.533093 1.000000
[47]:
ideal_drink_ahp = PartialAHP(ideal_pairwise_table)
drink_ahp.to_values().data
[47]:
Coffee    0.177746
Wine      0.019256
Tea       0.039269
Beer      0.116738
Soda      0.190395
Milk      0.129234
Water     0.327361
dtype: float64
[48]:
ideal_drink_ahp.consistency_ratio()
[48]:
-2.242874797222538e-16

Multi-Criteria Problem

[49]:
alternatives = ["Bethesda", "Boston", "Pittsburgh", "Santa Fe"]
weight_matrix = AdjacencyValueMatrix(
    [
        [1, 1/5, 3, 1/2, 5],
        [5, 1, 7, 1, 7],
        [1/3, 1/7, 1, 1/4, 3],
        [2, 1, 4, 1, 7],
        [1/5, 1/7, 1/3, 1/7, 1]
    ],
    vertices=["Culture", "Family", "Housing", "Job", "Transportation"]
)
criteria_matrices = {
    "Culture": AdjacencyValueMatrix(
        [
            [1, 1/2, 1, 1/2],
            [2, 1, 2.5, 1],
            [1, 1/2.5, 1, 1/2.5],
            [2, 1, 2.5, 1]
        ],
        vertices=alternatives
    ),

    "Family": AdjacencyValueMatrix(
        [
            [1, 2, 1/3, 4],
            [1, 1, 1/8, 2],
            [3, 8, 1, 9],
            [1/4, 1/2, 1/9, 1]
        ],
        vertices=alternatives
    ),
    "Housing": AdjacencyValueMatrix(
        [
            [1, 5, 1/2, 2.5],
            [1/5, 1, 1/9, 1/4],
            [2, 9, 1, 7],
            [1/2.5, 4, 1/7, 1]
        ],
        vertices=alternatives
    ),
    "Job": AdjacencyValueMatrix(
        [
            [1, 1/2, 3, 4],
            [2, 1, 6, 8],
            [1/3, 1/6, 1, 1],
            [1/4, 1/8, 1, 1]
        ],
        vertices=alternatives
    ),
    "Transportation": AdjacencyValueMatrix(
        [
            [1, 1.5, 1/2, 4],
            [1/1.5, 1, 1/3.5, 2.5],
            [2, 3.5, 1, 9],
            [1/4, 1/2.5, 1/9, 1]
        ],
        vertices=alternatives
    )
}
ahp = AHP(criteria_matrices, weight_matrix)
[50]:
for crit, _ahp in ahp.items():
    print(crit)
    print(_ahp.to_values().data)
    print(_ahp.consistency_ratio())
Culture
Bethesda      0.163455
Boston        0.345160
Pittsburgh    0.146224
Santa Fe      0.345160
dtype: float64
0.0023076203153888425
Family
Bethesda      0.202362
Boston        0.118736
Pittsburgh    0.624262
Santa Fe      0.054640
dtype: float64
0.10438952082920441
Housing
Bethesda      0.262283
Boston        0.046737
Pittsburgh    0.571307
Santa Fe      0.119674
dtype: float64
0.04472660419807111
Job
Bethesda      0.279304
Boston        0.558608
Pittsburgh    0.086848
Santa Fe      0.075240
dtype: float64
0.0038381119410735706
Transportation
Bethesda      0.248889
Boston        0.157034
Pittsburgh    0.532723
Santa Fe      0.061355
dtype: float64
0.0009863480199906254
[51]:
ahp.to_performance_table().data
[51]:
Culture Family Housing Job Transportation
Bethesda 0.163455 0.202362 0.262283 0.279304 0.248889
Boston 0.345160 0.118736 0.046737 0.558608 0.157034
Pittsburgh 0.146224 0.624262 0.571307 0.086848 0.532723
Santa Fe 0.345160 0.054640 0.119674 0.075240 0.061355
[52]:
ahp.weights
[52]:
{'Culture': 0.15216708320448322,
 'Family': 0.4334543680812483,
 'Housing': 0.07155647600567545,
 'Job': 0.3050064323898602,
 'Transportation': 0.03781564031873292}
[53]:
ahp.to_scores().data
[53]:
Bethesda      0.225956
Boston        0.283651
Pittsburgh    0.380355
Santa Fe      0.110038
dtype: float64
[ ]: