Using DEXiPy Data in Pandas and NumPy
Demonstration of how to convert DEXiPy decision alternatives to Pandas and NumPy.
Setup
Assuming DEXiPy has been installed in Python.
First some declarations.
[41]:
import pandas as pd
import numpy as np
from dexipy.dexi import read_dexi_from_string, evaluate
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.
[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 object contains alternatives - in this case, data of two cars. This data has not been printed above, but can be accessed as follows:
[43]:
dxi.alternatives
[43]:
[{'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}]
Notice that alternatives are already fully evaluated, so there is no need to evaluate() them.
Separate them in two variables:
[44]:
alt1 = dxi.alternatives[0]
alt2 = dxi.alternatives[1]
print(alt1)
print(alt2)
{'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}
Now make alternatives that will contain more varied data (None values, value sets and distributions). First, make a base alternative for evaluation, alt0:
[45]:
alt0 = dxi.alternative("Base",
values = {"BUY.PRICE": "low", "MAINT.PRICE": 1, "#PERS": 2, "#DOORS": 2},
LUGGAGE = "medium", SAFETY = "*")
print(alt0)
{'CAR': None, 'PRICE': None, 'BUY.PRICE': 'low', 'MAINT.PRICE': 1, 'TECH.CHAR.': None, 'COMFORT': None, '#PERS': 2, '#DOORS': 2, 'LUGGAGE': 'medium', 'SAFETY': '*', 'name': 'Base'}
Notice that alt0 has not been evaluated and contains None values. We shall leave it as it is.
Now make alternatives alts and altd. They are evaluations of alt0 using the “set” and “prob” methods, respectively.
[46]:
alts = dxi.evaluate(alt0, method = "set")
alts["name"] = "Set"
print(alts)
altd = dxi.evaluate(alt0, method = "prob")
alts["name"] = "Prob"
print(altd)
{'CAR': {0, 3}, 'PRICE': 2, 'BUY.PRICE': 2, 'MAINT.PRICE': 1, 'TECH.CHAR.': {0, 2, 3}, 'COMFORT': 2, '#PERS': 2, '#DOORS': 2, 'LUGGAGE': 1, 'SAFETY': {0, 1, 2}, 'name': 'Set'}
{'CAR': [0.3333333333333333, 0.0, 0.0, 0.6666666666666666], 'PRICE': 2, 'BUY.PRICE': 2, 'MAINT.PRICE': 1, 'TECH.CHAR.': [0.3333333333333333, 0.0, 0.3333333333333333, 0.3333333333333333], 'COMFORT': 2, '#PERS': 2, '#DOORS': 2, 'LUGGAGE': 1, 'SAFETY': {0, 1, 2}, 'name': 'Base'}
Converting alternatives to Pandas’ DataFrame
The main DEXiPy function for converting DEXiPy alternatives to data frames is dexipy.eval.columnize_alternatives.
Let us columnize the five alternatives:
[47]:
from dexipy.eval import columnize_alternatives
colalt = columnize_alternatives([alt1, alt2, alt0, alts, altd])
print(colalt)
{'name': ['Car1', 'Car2', 'Base', 'Prob', 'Base'], 'CAR': [3, 2, None, {0, 3}, [0.3333333333333333, 0.0, 0.0, 0.6666666666666666]], 'PRICE': [2, 1, None, 2, 2], 'BUY.PRICE': [1, 1, 'low', 2, 2], 'MAINT.PRICE': [2, 1, 1, 1, 1], 'TECH.CHAR.': [3, 2, None, {0, 2, 3}, [0.3333333333333333, 0.0, 0.3333333333333333, 0.3333333333333333]], 'COMFORT': [2, 2, None, 2, 2], '#PERS': [2, 2, 2, 2, 2], '#DOORS': [2, 2, 2, 2, 2], 'LUGGAGE': [2, 2, 'medium', 1, 1], 'SAFETY': [2, 1, '*', {0, 1, 2}, {0, 1, 2}]}
Columnized alternatives make a suitable argument to create a Panda’s DataFrame:
[49]:
dfalt = pd.DataFrame(colalt)
print(dfalt)
name CAR PRICE BUY.PRICE \
0 Car1 3 2.0 1
1 Car2 2 1.0 1
2 Base None NaN low
3 Prob {0, 3} 2.0 2
4 Base [0.3333333333333333, 0.0, 0.0, 0.6666666666666... 2.0 2
MAINT.PRICE TECH.CHAR. COMFORT \
0 2 3 2.0
1 1 2 2.0
2 1 None NaN
3 1 {0, 2, 3} 2.0
4 1 [0.3333333333333333, 0.0, 0.3333333333333333, ... 2.0
#PERS #DOORS LUGGAGE SAFETY
0 2 2 2 2
1 2 2 2 1
2 2 2 medium *
3 2 2 1 {0, 1, 2}
4 2 2 1 {0, 1, 2}
Panda detects data types itself, notice that dfalt types are somewhat varied:
[50]:
print(dfalt.dtypes)
name object
CAR object
PRICE float64
BUY.PRICE object
MAINT.PRICE int64
TECH.CHAR. object
COMFORT float64
#PERS int64
#DOORS int64
LUGGAGE object
SAFETY object
dtype: object
Converting more “benign” DEXiPy alternatives results in simpler data frames:
[54]:
colalt2 = columnize_alternatives([alt1, alt2])
dfalt2 = pd.DataFrame(colalt2)
print(dfalt2)
print("\n", dfalt2.dtypes)
name CAR PRICE BUY.PRICE MAINT.PRICE TECH.CHAR. COMFORT #PERS \
0 Car1 3 2 1 2 3 2 2
1 Car2 2 1 1 1 2 2 2
#DOORS LUGGAGE SAFETY
0 2 2 2
1 2 2 1
name object
CAR int64
PRICE int64
BUY.PRICE int64
MAINT.PRICE int64
TECH.CHAR. int64
COMFORT int64
#PERS int64
#DOORS int64
LUGGAGE int64
SAFETY int64
dtype: object
DEXiPy alternatives that contain value sets and distributions can be converted to numeric values using dexipy.eval.convert_alternatives():
[55]:
from dexipy.eval import convert_alternatives
cvagr = convert_alternatives(dxi, [alt1, alt2, alt0, alts, altd], aggregate = "mean")
# with some tweaking to exclude model root attribute and include "name":
colagr = columnize_alternatives(cvagr, attributes = ["name"] + dxi.non_root_ids)
dfagr = pd.DataFrame(colagr)
print(dfagr)
print("\n", dfagr.dtypes)
name CAR PRICE BUY.PRICE MAINT.PRICE TECH.CHAR. COMFORT #PERS \
0 Car1 1.000000 1.0 0.5 1.0 1.000000 1.0 1.0
1 Car2 0.666667 0.5 0.5 0.5 0.666667 1.0 1.0
2 Base NaN NaN NaN 0.5 NaN NaN 1.0
3 Prob 0.500000 1.0 1.0 0.5 0.555556 1.0 1.0
4 Base 0.666667 1.0 1.0 0.5 0.555556 1.0 1.0
#DOORS LUGGAGE SAFETY
0 0.666667 1.0 1.0
1 0.666667 1.0 0.5
2 0.666667 NaN NaN
3 0.666667 0.5 0.5
4 0.666667 0.5 0.5
name object
CAR float64
PRICE float64
BUY.PRICE float64
MAINT.PRICE float64
TECH.CHAR. float64
COMFORT float64
#PERS float64
#DOORS float64
LUGGAGE float64
SAFETY float64
dtype: object
It is also possible to make a DataFrame consisting of a textual interpretation of DEXi values using DexiModel.textualize_alternatives():
[56]:
txtalt = dxi.textualize_alternatives([alt1, alt2, alt0, alts, altd], decimals = 2)
coltxt = columnize_alternatives(txtalt, attributes = ["name"] + dxi.non_root_ids)
dftxt = pd.DataFrame(coltxt)
print(dftxt)
name CAR PRICE BUY.PRICE MAINT.PRICE \
0 Car1 exc low medium low
1 Car2 good medium medium medium
2 Base None None low medium
3 Prob ('unacc', 'exc') low low medium
4 Base {'unacc': 0.33, 'exc': 0.67} low low medium
TECH.CHAR. COMFORT #PERS #DOORS LUGGAGE \
0 exc high more 4 big
1 good high more 4 big
2 None None more 4 medium
3 ('bad', 'good', 'exc') high more 4 medium
4 {'bad': 0.33, 'good': 0.33, 'exc': 0.33} high more 4 medium
SAFETY
0 high
1 medium
2 *
3 ('small', 'medium', 'high')
4 ('small', 'medium', 'high')
This also works with text data:
[58]:
dftxt2 = pd.DataFrame(txtalt)
print(dftxt)
name CAR PRICE BUY.PRICE MAINT.PRICE \
0 Car1 exc low medium low
1 Car2 good medium medium medium
2 Base None None low medium
3 Prob ('unacc', 'exc') low low medium
4 Base {'unacc': 0.33, 'exc': 0.67} low low medium
TECH.CHAR. COMFORT #PERS #DOORS LUGGAGE \
0 exc high more 4 big
1 good high more 4 big
2 None None more 4 medium
3 ('bad', 'good', 'exc') high more 4 medium
4 {'bad': 0.33, 'good': 0.33, 'exc': 0.33} high more 4 medium
SAFETY
0 high
1 medium
2 *
3 ('small', 'medium', 'high')
4 ('small', 'medium', 'high')
Converting alternatives to NumPy’s ndarray
A direct conversion of some columnized alt data is achieved by list(alt.values()). This list can be used as an argument to create a numpy.array. The created array usually needs to be transposed using array.T. Depending on alt data items, specifying the array data type dtype might be necessary.
To start with simple columnized data:
[59]:
npalt2 = np.array(list(colalt2.values())).T
print(npalt2)
[['Car1' '3' '2' '1' '2' '3' '2' '2' '2' '2' '2']
['Car2' '2' '1' '1' '1' '2' '2' '2' '2' '2' '1']]
[60]:
npalt2 = np.array(list(colalt2.values()), dtype = 'O').T
print(npalt2)
[['Car1' 3 2 1 2 3 2 2 2 2 2]
['Car2' 2 1 1 1 2 2 2 2 2 1]]
[61]:
npagr = np.array(list(colagr.values())).T
print(npagr)
[['Car1' 1.0 1.0 0.5 1.0 1.0 1.0 1.0 0.6666666666666666 1.0 1.0]
['Car2' 0.6666666666666666 0.5 0.5 0.5 0.6666666666666666 1.0 1.0
0.6666666666666666 1.0 0.5]
['Base' None None None 0.5 None None 1.0 0.6666666666666666 None None]
['Prob' 0.5 1.0 1.0 0.5 0.5555555555555556 1.0 1.0 0.6666666666666666
0.5 0.5]
['Base' 0.6666666666666666 1.0 1.0 0.5 0.5555555555555555 1.0 1.0
0.6666666666666666 0.5 0.5]]
For columnized data that contains value sets and distributions:
[62]:
npalt = np.array(list(colalt.values()), dtype = 'O').T
print(npalt)
[['Car1' 3 2 1 2 3 2 2 2 2 2]
['Car2' 2 1 1 1 2 2 2 2 2 1]
['Base' None None 'low' 1 None None 2 2 'medium' '*']
['Prob' {0, 3} 2 2 1 {0, 2, 3} 2 2 2 1 {0, 1, 2}]
['Base' list([0.3333333333333333, 0.0, 0.0, 0.6666666666666666]) 2 2 1
list([0.3333333333333333, 0.0, 0.3333333333333333, 0.3333333333333333])
2 2 2 1 {0, 1, 2}]]
Conversion from textual data:
[63]:
nptxt = np.array(list(coltxt.values())).T
print(nptxt)
[['Car1' 'exc' 'low' 'medium' 'low' 'exc' 'high' 'more' '4' 'big' 'high']
['Car2' 'good' 'medium' 'medium' 'medium' 'good' 'high' 'more' '4' 'big'
'medium']
['Base' 'None' 'None' 'low' 'medium' 'None' 'None' 'more' '4' 'medium'
'*']
['Prob' "('unacc', 'exc')" 'low' 'low' 'medium' "('bad', 'good', 'exc')"
'high' 'more' '4' 'medium' "('small', 'medium', 'high')"]
['Base' "{'unacc': 0.33, 'exc': 0.67}" 'low' 'low' 'medium'
"{'bad': 0.33, 'good': 0.33, 'exc': 0.33}" 'high' 'more' '4' 'medium'
"('small', 'medium', 'high')"]]
Another option is to make conversions from Panda’s data frames using DataFrame.to_numpy:
[64]:
npalt = dfalt.to_numpy()
print(npalt)
[['Car1' 3 2.0 1 2 3 2.0 2 2 2 2]
['Car2' 2 1.0 1 1 2 2.0 2 2 2 1]
['Base' None nan 'low' 1 None nan 2 2 'medium' '*']
['Prob' {0, 3} 2.0 2 1 {0, 2, 3} 2.0 2 2 1 {0, 1, 2}]
['Base' list([0.3333333333333333, 0.0, 0.0, 0.6666666666666666]) 2.0 2 1
list([0.3333333333333333, 0.0, 0.3333333333333333, 0.3333333333333333])
2.0 2 2 1 {0, 1, 2}]]
[65]:
npagr = dfagr.to_numpy()
print(npagr)
[['Car1' 1.0 1.0 0.5 1.0 1.0 1.0 1.0 0.6666666666666666 1.0 1.0]
['Car2' 0.6666666666666666 0.5 0.5 0.5 0.6666666666666666 1.0 1.0
0.6666666666666666 1.0 0.5]
['Base' nan nan nan 0.5 nan nan 1.0 0.6666666666666666 nan nan]
['Prob' 0.5 1.0 1.0 0.5 0.5555555555555556 1.0 1.0 0.6666666666666666
0.5 0.5]
['Base' 0.6666666666666666 1.0 1.0 0.5 0.5555555555555555 1.0 1.0
0.6666666666666666 0.5 0.5]]
[66]:
nptxt = dftxt.to_numpy()
print(nptxt)
[['Car1' 'exc' 'low' 'medium' 'low' 'exc' 'high' 'more' '4' 'big' 'high']
['Car2' 'good' 'medium' 'medium' 'medium' 'good' 'high' 'more' '4' 'big'
'medium']
['Base' 'None' 'None' 'low' 'medium' 'None' 'None' 'more' '4' 'medium'
'*']
['Prob' "('unacc', 'exc')" 'low' 'low' 'medium' "('bad', 'good', 'exc')"
'high' 'more' '4' 'medium' "('small', 'medium', 'high')"]
['Base' "{'unacc': 0.33, 'exc': 0.67}" 'low' 'low' 'medium'
"{'bad': 0.33, 'good': 0.33, 'exc': 0.33}" 'high' 'more' '4' 'medium'
"('small', 'medium', 'high')"]]
[ ]:
[ ]: