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')"]]
[ ]:
[ ]: