DEXiPy: Car Demo
Using the standard Car evaluation model, distributed with DEXi software (http://kt.ijs.si/MarkoBohanec/dexi.html)
Assuming DEXiPy has been installed in Python.
First some declarations.
[1]:
import dexipy.utils as utl
from dexipy.dexi import read_dexi, read_dexi_from_string, evaluate
from dexipy.eval import write_alternatives
from dexipy.tests.testdata import CAR_XML
Read and print the Car model. In this case, read it from an XML string, imported from dexipy.tests.testdata. Alternatively, you may read it from a .dxi
file: dxi = read_dexi("path-to-file")
.
[2]:
dxi = read_dexi_from_string(CAR_XML)
print(dxi)
DEXi Model: CAR_MODEL
Description: Car demo
index id structure scale funct
0 CAR_MODEL CAR_MODEL
1 CAR +- CAR unacc; acc; good; exc (+) 12 3x4
2 PRICE |- PRICE high; medium; low (+) 9 3x3
3 BUY.PRICE | |- BUY.PRICE high; medium; low (+)
4 MAINT.PRICE | +- MAINT.PRICE high; medium; low (+)
5 TECH.CHAR. +- TECH.CHAR. bad; acc; good; exc (+) 9 3x3
6 COMFORT |- COMFORT small; medium; high (+) 36 3x4x3
7 #PERS | |- #PERS to_2; 3-4; more (+)
8 #DOORS | |- #DOORS 2; 3; 4; more (+)
9 LUGGAGE | +- LUGGAGE small; medium; big (+)
10 SAFETY +- SAFETY small; medium; high (+)
In addition to the model itself, dxi
contains alternatives - in this case, data of two cars. This data is not printed, but can be accessed as follows:
[3]:
dxi.alternatives
[3]:
[{'name': 'Car1',
'CAR': 3,
'PRICE': 2,
'BUY.PRICE': 1,
'MAINT.PRICE': 2,
'TECH.CHAR.': 3,
'COMFORT': 2,
'#PERS': 2,
'#DOORS': 2,
'LUGGAGE': 2,
'SAFETY': 2},
{'name': 'Car2',
'CAR': 2,
'PRICE': 1,
'BUY.PRICE': 1,
'MAINT.PRICE': 1,
'TECH.CHAR.': 2,
'COMFORT': 2,
'#PERS': 2,
'#DOORS': 2,
'LUGGAGE': 2,
'SAFETY': 1}]
Checking the correctness of alternatives data:
[4]:
check = dxi.check_alternatives()
print(utl.check_str(check, warnings = True))
Print alternatives in a tabular form, using internal representation of DEXi values:
[5]:
print(dxi.alt_table())
alternative Car1 Car2
CAR 3 2
PRICE 2 1
BUY.PRICE 1 1
MAINT.PRICE 2 1
TECH.CHAR. 3 2
COMFORT 2 2
#PERS 2 2
#DOORS 2 2
LUGGAGE 2 2
SAFETY 2 1
Or, alternatively, use textual representation:
[6]:
print(dxi.alt_text())
alternative Car1 Car2
CAR exc good
PRICE low medium
BUY.PRICE medium medium
MAINT.PRICE low medium
TECH.CHAR. exc good
COMFORT high high
#PERS more more
#DOORS 4 4
LUGGAGE big big
SAFETY high medium
Model components
Now inspect some properties of dxi
, which is an object of class DexiModel
:
[7]:
print("name: ", dxi.name)
print("description: ", dxi.description)
print("linking: ", dxi.linking)
print("att_ids: ", dxi.att_ids)
print("aggregate_ids: ", dxi.aggregate_ids)
print("basic_ids: ", dxi.basic_ids)
print("linked_ids: ", dxi.link_ids)
print("non_root_ids: ", dxi.non_root_ids)
print("root.id: ", dxi.root.id)
print("natt: ", dxi.natt)
print("attributes[1].id: ", dxi.attributes[1].id)
name: CAR_MODEL
description: Car demo
linking: False
att_ids: ['CAR_MODEL', 'CAR', 'PRICE', 'BUY.PRICE', 'MAINT.PRICE', 'TECH.CHAR.', 'COMFORT', '#PERS', '#DOORS', 'LUGGAGE', 'SAFETY']
aggregate_ids: ['CAR', 'PRICE', 'TECH.CHAR.', 'COMFORT']
basic_ids: ['BUY.PRICE', 'MAINT.PRICE', '#PERS', '#DOORS', 'LUGGAGE', 'SAFETY']
linked_ids: []
non_root_ids: ['CAR', 'PRICE', 'BUY.PRICE', 'MAINT.PRICE', 'TECH.CHAR.', 'COMFORT', '#PERS', '#DOORS', 'LUGGAGE', 'SAFETY']
root.id: CAR_MODEL
natt: 11
attributes[1].id: CAR
Find a particular attribute:
[8]:
price = dxi.attrib("PRICE")
att5 = dxi.attrib(5)
print("price attribute: ", price.id)
print("att5 attribute: ", att5.id)
price attribute: PRICE
att5 attribute: TECH.CHAR.
Inspect some properties and methods of price
, which is an object of class DexiAttribute
:
[9]:
att = price
print("name: ", att.name) # name as in the original DEXi model
# id is by default equal to self.name, but may have been changed so that all ids in the model are unique
print("id: ", att.id)
print("description: ", att.description)
print("parent.id: ", att.parent.id)
print("ninp() ", att.ninp())
print("inputs[0].id: ", att.inputs[0].id)
print("dim() ", att.dim())
print("is_basic() ", att.is_basic())
print("is_aggregate() ", att.is_aggregate())
print("is_link() ", att.is_link())
print("level() ", att.level())
print("affects(CAR) ", att.affects(dxi.attrib("CAR")))
print("affects(SAFETY) ", att.affects(dxi.attrib("SAFETY")))
name: PRICE
id: PRICE
description: Price of a car
parent.id: CAR
ninp() 2
inputs[0].id: BUY.PRICE
dim() [3, 3]
is_basic() False
is_aggregate() True
is_link() False
level() 2
affects(CAR) True
affects(SAFETY) False
When fully defined, attributes have assigned scales (DexiScale
class):
[10]:
print(price.scale, "\n")
print("scale values: ",price.scale.values)
print("scale classes: ",price.scale.quality)
<dexipy.dexi.DexiDiscreteScale object at 0x00000243A2561D60>
scale values: ['high', 'medium', 'low']
scale classes: [<DexiQuality.BAD: -1>, <DexiQuality.NONE: 0>, <DexiQuality.GOOD: 1>]
Aggregate attributes may have assigned functions (DexiFunction
class):
[11]:
print(price.funct, "\n")
print("funct values: ",price.funct.values)
<dexipy.dexi.DexiTabularFunction object at 0x00000243A2561970>
funct values: {(0, 0): 0, (0, 1): 0, (0, 2): 0, (1, 0): 0, (1, 1): 1, (1, 2): 2, (2, 0): 0, (2, 1): 2, (2, 2): 2}
Evaluation of alternatives
Now, let’s define and evaluate our own alternatives (cars).
First, a simple one, using dxi.alternative()
and illustrating two points: - combining dictionary and keyword arguments - combining numeric and string input values
[12]:
alt = dxi.alternative("MySimpleCar",
values = {"BUY.PRICE": "low", "MAINT.PRICE": 1, "#PERS": 2, "#DOORS": 2},
LUGGAGE = "medium", SAFETY = 1)
print(alt)
print("")
print(dxi.alt_table(alt))
print("")
print(dxi.alt_text(alt))
{'CAR': None, 'PRICE': None, 'BUY.PRICE': 'low', 'MAINT.PRICE': 1, 'TECH.CHAR.': None, 'COMFORT': None, '#PERS': 2, '#DOORS': 2, 'LUGGAGE': 'medium', 'SAFETY': 1, 'name': 'MySimpleCar'}
alternative MySimpleCar
CAR None
PRICE None
BUY.PRICE low
MAINT.PRICE 1
TECH.CHAR. None
COMFORT None
#PERS 2
#DOORS 2
LUGGAGE medium
SAFETY 1
alternative MySimpleCar
CAR None
PRICE None
BUY.PRICE low
MAINT.PRICE medium
TECH.CHAR. None
COMFORT None
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Evaluate alt
and see what comes out:
[13]:
eval_alt = dxi.evaluate(alt)
print("")
print(dxi.alt_table(eval_alt))
print("")
print(dxi.alt_text(eval_alt))
alternative MySimpleCar
CAR 3
PRICE 2
BUY.PRICE 2
MAINT.PRICE 1
TECH.CHAR. 2
COMFORT 2
#PERS 2
#DOORS 2
LUGGAGE 1
SAFETY 1
alternative MySimpleCar
CAR exc
PRICE low
BUY.PRICE low
MAINT.PRICE medium
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Advanced evaluation methods
Evaluation using sets:
[14]:
alts1 = dxi.alternative("MyCarSet1", alt = alt, SAFETY = "*") # alt = alt copies values from variable alt defined above
eval_alts1 = dxi.evaluate(alts1)
print(dxi.alt_text(eval_alts1))
alternative MyCarSet1
CAR ('unacc', 'exc')
PRICE low
BUY.PRICE low
MAINT.PRICE medium
TECH.CHAR. ('bad', 'good', 'exc')
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY ('small', 'medium', 'high')
[15]:
alts2 = dxi.alternative("MyCarSet2", alt = alt, SAFETY = {1, 2})
eval_alts2 = dxi.evaluate(alts2)
print(dxi.alt_text(eval_alts2))
alternative MyCarSet2
CAR exc
PRICE low
BUY.PRICE low
MAINT.PRICE medium
TECH.CHAR. ('good', 'exc')
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY ('medium', 'high')
Evaluation using distributions. First define an alternative:
[16]:
altd = dxi.alternative("MyCarDistr", alt = alt, values = {"MAINT.PRICE": [0.1, 0.6, 0.3]})
print(dxi.alt_text(altd))
alternative MyCarDistr
CAR None
PRICE None
BUY.PRICE low
MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
TECH.CHAR. None
COMFORT None
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Evaluate using method "set"
(default: interpreting distributions as sets):
[17]:
eval_altds = dxi.evaluate(altd, method = "set")
print(dxi.alt_text(eval_altds))
alternative MyCarDistr
CAR ('unacc', 'exc')
PRICE ('high', 'low')
BUY.PRICE low
MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Evaluate using method "prob"
(interpreting distributions as probability distributions):
[18]:
eval_altdp = dxi.evaluate(altd, method = "prob")
print(dxi.alt_text(eval_altdp, decimals = 2))
alternative MyCarDistr
CAR {'unacc': 0.1, 'exc': 0.9}
PRICE {'high': 0.1, 'low': 0.9}
BUY.PRICE low
MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Evaluate using method "fuzzy"
(interpreting distributions as fuzzy distributions):
[19]:
eval_altdf = dxi.evaluate(altd, method = "fuzzy")
print(dxi.alt_text(eval_altdf))
alternative MyCarDistr
CAR {'unacc': 0.1, 'exc': 0.6}
PRICE {'high': 0.1, 'low': 0.6}
BUY.PRICE low
MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Evaluate using method "fuzzynorm"
(interpreting distributions as fuzzy distributions, force fuzzy normalization):
[20]:
eval_altdfn = dxi.evaluate(altd, method = "fuzzynorm")
print(dxi.alt_text(eval_altdfn, decimals = 2, use_dict = False))
alternative MyCarDistr
CAR [0.17, 0.0, 0.0, 1.0]
PRICE [0.17, 0.0, 1.0]
BUY.PRICE low
MAINT.PRICE [0.17, 1.0, 0.5]
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Pruning evaluation at PRICE
:
[21]:
palt = dxi.alternative("MyPrunedCar", alt = alt, PRICE = "low") # treating PRICE as a basic attribute
eval_palt = dxi.evaluate(palt, prune = ["PRICE"])
print(dxi.alt_text(eval_palt))
alternative MyPrunedCar
CAR exc
PRICE low
BUY.PRICE None
MAINT.PRICE None
TECH.CHAR. good
COMFORT high
#PERS more
#DOORS 4
LUGGAGE medium
SAFETY medium
Alternatives may be grouped in a list and evaluated at once:
[22]:
car_alts = [alt, alts1, alts2, altd, palt]
eval_car_alts = dxi.evaluate(car_alts)
print(dxi.alt_table(eval_car_alts))
alternative MySimpleCar MyCarSet1 MyCarSet2 MyCarDistr MyPrunedCar
CAR 3 {0, 3} 3 {0, 3} 3
PRICE 2 2 2 {0, 2} 2
BUY.PRICE 2 2 2 2 2
MAINT.PRICE 1 1 1 [0.1, 0.6, 0.3] 1
TECH.CHAR. 2 {0, 2, 3} {2, 3} 2 2
COMFORT 2 2 2 2 2
#PERS 2 2 2 2 2
#DOORS 2 2 2 2 2
LUGGAGE 1 1 1 1 1
SAFETY 1 {0, 1, 2} {1, 2} 1 1
Transposed and partial printout is also possible:
[23]:
print(dxi.alt_text(eval_car_alts, aggregate = True, transpose = True))
alternative CAR PRICE TECH.CHAR. COMFORT
MySimpleCar exc low good high
MyCarSet1 ('unacc', 'exc') low ('bad', 'good', 'exc') high
MyCarSet2 exc low ('good', 'exc') high
MyCarDistr ('unacc', 'exc') ('high', 'low') good high
MyPrunedCar exc low good high
Exporting alternatives’ data
Alternatives, created and/or edited in DexiPy, can be written out so that they can be subsequently imported by DEXi/DEXiWin software.
Here we show writing out data to the console.For writing contents to external files, a filename
argument has to be provided, for instance:
write_alternatives(dxi, alternatives, filename = "dir/cardata.tab", delimiter = "\t")
Let’s see a tab-delimited output:
[24]:
write_alternatives(dxi, delimiter = "\t")
name Car1 Car2
CAR 4 3
. PRICE 3 2
. . BUY.PRICE 2 2
. . MAINT.PRICE 3 2
. TECH.CHAR. 4 3
. . COMFORT 3 3
. . . #PERS 3 3
. . . #DOORS 3 3
. . . LUGGAGE 3 3
. . SAFETY 3 2
And in a “csv” “invariant” format:
[25]:
write_alternatives(dxi, delimiter = ",")
name,Car1,Car2
CAR,4,3
. PRICE,3,2
. . BUY.PRICE,2,2
. . MAINT.PRICE,3,2
. TECH.CHAR.,4,3
. . COMFORT,3,3
. . . #PERS,3,3
. . . #DOORS,3,3
. . . LUGGAGE,3,3
. . SAFETY,3,2
Analysis of alternatives
Selective Explanation
Prints out particularly bad and particularly good evaluations.
Selective explanation of two dxi
Cars:
[26]:
dxi.selective_explanation()
Alternative Car1
Weak points
None
Strong points
attribute Car1
+-CAR exc
|-PRICE low
| +-MAINT.PRICE low
+-TECH.CHAR. exc
|-COMFORT high
| |-#PERS more
| +-LUGGAGE big
+-SAFETY high
Alternative Car2
Weak points
None
Strong points
attribute Car2
|-COMFORT high
| |-#PERS more
| +-LUGGAGE big
Selective explanation of some other evaluated alternatives:
[27]:
dxi.selective_explanation([eval_altds, eval_altdp, eval_altdf])
Alternative MyCarDistr
Weak points
atribute MyCarDistr
+-CAR ('unacc', 'exc')
|-PRICE ('high', 'low')
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
Strong points
attribute MyCarDistr
+-CAR ('unacc', 'exc')
|-PRICE ('high', 'low')
| |-BUY.PRICE low
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
|-COMFORT high
| |-#PERS more
Alternative MyCarDistr
Weak points
atribute MyCarDistr
+-CAR {'unacc': 0.10000000000000002, 'exc': 0.9}
|-PRICE {'high': 0.10000000000000002, 'low': 0.9}
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
Strong points
attribute MyCarDistr
+-CAR {'unacc': 0.10000000000000002, 'exc': 0.9}
|-PRICE {'high': 0.10000000000000002, 'low': 0.9}
| |-BUY.PRICE low
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
|-COMFORT high
| |-#PERS more
Alternative MyCarDistr
Weak points
atribute MyCarDistr
+-CAR {'unacc': 0.1, 'exc': 0.6}
|-PRICE {'high': 0.1, 'low': 0.6}
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
Strong points
attribute MyCarDistr
+-CAR {'unacc': 0.1, 'exc': 0.6}
|-PRICE {'high': 0.1, 'low': 0.6}
| |-BUY.PRICE low
| +-MAINT.PRICE {'high': 0.1, 'medium': 0.6, 'low': 0.3}
|-COMFORT high
| |-#PERS more
Plus/Minus Analysis
Investigating the effects of changing one attribute value at a time.
Plus/Minus analysis of dxi.alternatives[0]
:
[28]:
alt0 = dxi.alternatives[0]
dxi.plus_minus(alt0)
Attribute -2 -1 Car1 +1 +2
CAR exc
| |-BUY.PRICE [ unacc medium ]
| +-MAINT.PRICE unacc low ]
| |-#PERS unacc more ]
| |-#DOORS unacc 4 ]
| +-LUGGAGE unacc big ]
+-SAFETY unacc high ]
Plus/Minus analysis of dxi.alternatives[1]
, this time observing the effects on "TECH.CHAR."
and using different parameters:
[29]:
alt1 = dxi.alternatives[1]
dxi.plus_minus(alt1, target = dxi.attrib("TECH.CHAR."), structure = False, text = False, minus = 1, plus = 1)
Attribute -1 Car2 +1
TECH.CHAR. 2
#PERS 2 ]
#DOORS 2
LUGGAGE 2 ]
SAFETY 0 1 3
Compare Alternatives
Compare an alternative with other alternatives.
First compare alt0
(“Car1”) with alt1
(“Car2”):
[30]:
dxi.compare_alternatives(alt0, alt1)
Attribute Car1 Car2
+-CAR exc > good
|-PRICE low > medium
| |-BUY.PRICE medium =
| +-MAINT.PRICE low > medium
+-TECH.CHAR. exc > good
|-COMFORT high =
| |-#PERS more =
| |-#DOORS 4 =
| +-LUGGAGE big =
+-SAFETY high > medium
Slightly modify “Car2” and compare “Car2” with “Car1” and “Car2”:
[31]:
alt2 = dxi.evaluate(dxi.alternative(name = "Car2*", alt = alt1, values = {"#PERS": "3-4"}))
dxi.compare_alternatives(alt1, [alt0, alt2])
Attribute Car2 Car1 Car2*
+-CAR good < exc =
|-PRICE medium < low =
| |-BUY.PRICE medium = =
| +-MAINT.PRICE medium < low =
+-TECH.CHAR. good < exc =
|-COMFORT high = >=
| |-#PERS more = > 3-4
| |-#DOORS 4 = =
| +-LUGGAGE big = =
+-SAFETY medium < high =
Repeat the above comparison, but with deep = False
. See the difference?
[32]:
dxi.compare_alternatives(alt1, [alt0, alt2], deep = False)
Attribute Car2 Car1 Car2*
+-CAR good < exc =
|-PRICE medium < low =
| |-BUY.PRICE medium = =
| +-MAINT.PRICE medium < low =
+-TECH.CHAR. good < exc =
|-COMFORT high = =
| |-#PERS more = > 3-4
| |-#DOORS 4 = =
| +-LUGGAGE big = =
+-SAFETY medium < high =
Demonstrating some other arguments:
[33]:
dxi.compare_alternatives(alt1, [alt0, alt2], root = dxi.attrib("COMFORT"), structure = False, text = False)
Attribute Car2 Car1 Car2*
COMFORT 2 = >=
#PERS 2 = > 1
#DOORS 2 = =
LUGGAGE 2 = =
[34]:
dxi.compare_alternatives(alt1, [alt0, alt2], prune = ["COMFORT"], compare = False)
Attribute Car2 Car1 Car2*
+-CAR good exc
|-PRICE medium low
| |-BUY.PRICE medium
| +-MAINT.PRICE medium low
+-TECH.CHAR. good exc
|-COMFORT high
+-SAFETY medium high
Charts
Draw alternatives with respect to one attribute
Draw overall evaluation results of cars. Notice that some values are value distributions.
[35]:
from dexipy.charts import plotalt1
eval_cars2 = eval_car_alts
eval_cars2.extend([eval_altds, eval_altdp, eval_altdf])
plotalt1(dxi, None, eval_cars2)
Similar display for the attribute “MAINT.PRICE”:
[36]:
plotalt1(dxi, "MAINT.PRICE", eval_cars2)
Now taking a model that has continuous attributes:
[37]:
from dexipy.tests.testdata import CONTINUOUS_NEW_XML
cnt = read_dexi_from_string(CONTINUOUS_NEW_XML)
plotalt1(cnt)
The cnt.first()
attribute “OneLevel” (above) is discrete. But “N1” is continuous:
[38]:
plotalt1(cnt, "N1")
Draw a scatterplot, considering two attributes
[39]:
from dexipy.charts import plotalt2
plotalt2(dxi, "TECH.CHAR.", "PRICE")
plotalt2(dxi, "BUY.PRICE", "MAINT.PRICE")
Plot alternatives evaluated as sets or distributions:
[40]:
plotalt2(dxi, "TECH.CHAR.", "PRICE", [eval_alts1, eval_alts2, eval_altdp])
Draw alternatives using parallel axes
[41]:
from dexipy.charts import plotalt_parallel
plotalt_parallel(dxi, shift = 0.02, aggregate = "mean")
[42]:
plotalt_parallel(dxi, alternatives = [eval_alts1, eval_alts2, eval_altdp], aggregate = "minmax", shift = 0.01)
[43]:
plotalt_parallel(dxi, alternatives = [eval_alts1, eval_alts2, eval_altdp], aggregate = "mean", shift = 0.01)
[44]:
plotalt_parallel(dxi, dxi.basic, alternatives = [eval_alts1, eval_alts2, eval_altdp], shift = 0.01)
Draw alternatives using a radar chart
[45]:
from dexipy.charts import plotalt_radar
plotalt_radar(dxi, shift = 0.02, fill = 0.1)
[46]:
plotalt_radar(dxi, ["CAR", "TECH.CHAR.", "PRICE"], figsize = (3, 3))
[47]:
plotalt_radar(dxi, None, [eval_alts1], fill = 0.1)
[48]:
plotalt_radar(dxi, None, [eval_alts1, eval_altdp], aggregate = "mean", inner = 0)
[49]:
plotalt_radar(dxi, list(reversed(dxi.basic_ids)), [eval_alts1, eval_altdp], aggregate = "mean",
inner = 0, figsize = (6,3), legend_pos = (1.3, 0.5), legend_loc = "center left")
plotalt_radar(dxi, dxi.aggregate, [eval_alts1, eval_altdp], aggregate = "mean", inner = 0, figsize = (3, 3))
[ ]: