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

[ ]: