# 1. Digraph3 Tutorials

- Author
Raymond Bisdorff, Emeritus Professor of Applied Mathematics and Computer Science, https://rbisdorff.github.io/

- Version
Python 3.10 (release: 3.10.4)

- PDF version
- Copyright
R. Bisdorff 2013-2022

## 1.1. Contents

**Working with digraphs and outranking digraphs****Evaluation and decision methods and tools****Evaluation and decision case studies****Moving on to undirected graphs****Appendices**

**Preface**

The tutorials in this document describe the practical usage of our *Digraph3* Python3 software resources in the field of *Algorithmic Decision Theory* and more specifically in **outranking** based *Multiple Criteria Decision Aid* (MCDA). They mainly illustrate practical tools for a Master Course on Algorithmic Decision Theory at the University of Luxembourg.

The document contains first a set of tutorials introducing the main objects available in the Digraph3 collection of Python3 modules, like **digraphs**, **outranking digraphs**, **performance tableaux** and **voting profiles**. Some of the tutorials are decision problem oriented and show how to compute the potential **winner(s)** of an election, how to build a **best choice recommendation**, or how to **rate** or **linearly rank** with multiple incommensurable performance criteria.

More graph theoretical tutorials follow. One on working with **undirected graphs**, followed by a tutorial on how to compute **non isomorphic maximal independent sets** (kernels) in the n-cycle graph. Another tutorial is furthermore devoted on how to generally compute **kernels** in graphs, digraphs and, more specifically, *initial* and *terminal* kernels in outranking digraphs. Special tutorials are devoted to *perfect* graphs, like *split*, *interval* and *permutation* graphs, and to *tree-graphs* and *forests*.

## 1.2. Working with the *Digraph3* software resources

### 1.2.1. Purpose

The basic idea of the Digraph3 Python resources is to make easy python interactive sessions or write short Python3 scripts for computing all kind of results from a bipolar-valued digraph or graph. These include such features as maximal independent, maximal dominant or absorbent choices, rankings, outrankings, linear ordering, etc. Most of the available computing resources are meant to illustrate a Master Course on Algorithmic Decision Theory given at the University of Luxembourg in the context of its *Master in Information and Computer Science* (MICS).

The Python development of these computing resources offers the advantage of an easy to write and maintain OOP source code as expected from a performing scripting language without loosing on efficiency in execution times compared to compiled languages such as C++ or Java.

### 1.2.2. Downloading of the Digraph3 resources

Using the Digraph3 modules is easy. You only need to have installed on your system the Python programming language of version 3.+ (readily available under Linux and Mac OS). Notice that, from Version 3.3 on, the Python standard decimal module implements very efficiently its decimal.Decimal class in C. Now, Decimal objects are mainly used in the Digraph3 characteristic r-valuation functions, which makes the recent python-3.7+ versions much faster (more than twice as fast) when extensive digraph operations are performed.

Several download options (easiest under Linux or Mac OS-X) are given.

Either, by using a git client either, from github

```
...$ git clone https://github.com/rbisdorff/Digraph3
```

Or, from sourceforge.net

```
...$ git clone https://git.code.sf.net/p/digraph3/code Digraph3
```

Or, with a browser access, download and extract the latest distribution zip archive either, from the github link above or, from the sourceforge page .

See the Installation section in the Technical Reference.

### 1.2.3. Starting a Python3 session

You may start an interactive Python3 session in the *Digraph3* directory.

```
1$HOME/.../Digraph3$ python3
2Python 3.10.0 (default, Oct 21 2021, 10:53:53)
3[GCC 11.2.0] on linux Type "help", "copyright",
4"credits" or "license" for more information.
5>>>
```

For exploring the classes and methods provided by the *Digraph3* modules (see the Reference manual) enter the *Python3* commands following the session prompts marked with `>>>`

or `...`

. The lines without the prompt are output from the Python3 interpreter.

```
1>>> from randomDigraphs import RandomDigraph
2>>> dg = RandomDigraph(order=5,arcProbability=0.5,seed=101)
3>>> dg
4 *------- Digraph instance description ------*
5 Instance class : RandomDigraph
6 Instance name : randomDigraph
7 Digraph Order : 5
8 Digraph Size : 12
9 Valuation domain : [-1.00; 1.00]
10 Determinateness : 100.000
11 Attributes : ['actions', 'valuationdomain', 'relation',
12 'order', 'name', 'gamma', 'notGamma',
13 'seed', 'arcProbability', ]
```

In Listing 1.1 we import, for instance, from the `randomDigraphs`

module the `RandomDigraph`

class in order to generate a random *digraph* object *dg* of order 5 - number of nodes called (decision) *actions* - and arc probability of 50%. We may directly inspect the content of python object *dg* (Line 3).

Note

For convenience of redoing the computations, all python code-blocks show in the upper right corner a specific **copy button** which allows to both copy *only* code lines, i.e. lines starting with ‘>>>’ or ‘…’, and stripping the console prompts. The copied code lines may hence be right away *pasted* into a Python console session.

### 1.2.4. `Digraph`

object structure

All `Digraph`

objects contain at least the following attributes (see Listing 1.1 Lines 11-12):

A

**name**attribute, holding usually the actual name of the stored instance that was used to create the instance;A ordered dictionary of digraph nodes called

**actions**(decision alternatives) with at least a ‘name’ attribute;An

**order**attribute containing the number of graph nodes (length of the actions dictionary) automatically added by the object constructor;A logical characteristic

**valuationdomain**dictionary with three decimal entries: the minimum (-1.0, means certainly false), the median (0.0, means missing information) and the maximum characteristic value (+1.0, means certainly true);A double dictionary called

**relation**and indexed by an oriented pair of actions (nodes) and carrying a decimal characteristic value in the range of the previous valuation domain;Its associated

**gamma**attribute, a dictionary containing the direct successors, respectively predecessors of each action, automatically added by the object constructor;Its associated

**notGamma**attribute, a dictionary containing the actions that are not direct successors respectively predecessors of each action, automatically added by the object constructor.

See the technical documentation of the root `digraphs.Digraph`

class.

### 1.2.5. Permanent storage

The `save()`

method stores the digraph object *dg* in a file named ‘tutorialDigraph.py’,

```
>>> dg.save('tutorialDigraph')
*--- Saving digraph in file: <tutorialDigraph.py> ---*
```

with the following content

```
1from decimal import Decimal
2from collections import OrderedDict
3actions = OrderedDict([
4 ('a1', {'shortName': 'a1', 'name': 'random decision action'}),
5 ('a2', {'shortName': 'a2', 'name': 'random decision action'}),
6 ('a3', {'shortName': 'a3', 'name': 'random decision action'}),
7 ('a4', {'shortName': 'a4', 'name': 'random decision action'}),
8 ('a5', {'shortName': 'a5', 'name': 'random decision action'}),
9 ])
10valuationdomain = {'min': Decimal('-1.0'),
11 'med': Decimal('0.0'),
12 'max': Decimal('1.0'),
13 'hasIntegerValuation': True, # repr. format
14 }
15relation = {
16 'a1': {'a1':Decimal('-1.0'), 'a2':Decimal('-1.0'),
17 'a3':Decimal('1.0'), 'a4':Decimal('-1.0'),
18 'a5':Decimal('-1.0'),},
19 'a2': {'a1':Decimal('1.0'), 'a2':Decimal('-1.0'),
20 'a3':Decimal('-1.0'), 'a4':Decimal('1.0'),
21 'a5':Decimal('1.0'),},
22 'a3': {'a1':Decimal('1.0'), 'a2':Decimal('-1.0'),
23 'a3':Decimal('-1.0'), 'a4':Decimal('1.0'),
24 'a5':Decimal('-1.0'),},
25 'a4': {'a1':Decimal('1.0'), 'a2':Decimal('1.0'),
26 'a3':Decimal('1.0'), 'a4':Decimal('-1.0'),
27 'a5':Decimal('-1.0'),},
28 'a5': {'a1':Decimal('1.0'), 'a2':Decimal('1.0'),
29 'a3':Decimal('1.0'), 'a4':Decimal('-1.0'),
30 'a5':Decimal('-1.0'),},
31 }
```

### 1.2.6. Inspecting a `Digraph`

object

We may reload (see Listing 1.2) the previously saved digraph object from the file named ‘tutorialDigraph.py’ with the `Digraph`

class constructor and different *show* methods (see Listing 1.2 below) reveal us that *dg* is a *crisp*, *irreflexive* and *connected* digraph of *order* five.

```
1>>> from digraphs import Digraph
2>>> dg = Digraph('tutorialDigraph')
3>>> dg.showShort()
4 *----- show short -------------*
5 Digraph : tutorialDigraph
6 Actions : OrderedDict([
7 ('a1', {'shortName': 'a1', 'name': 'random decision action'}),
8 ('a2', {'shortName': 'a2', 'name': 'random decision action'}),
9 ('a3', {'shortName': 'a3', 'name': 'random decision action'}),
10 ('a4', {'shortName': 'a4', 'name': 'random decision action'}),
11 ('a5', {'shortName': 'a5', 'name': 'random decision action'})
12 ])
13 Valuation domain : {
14 'min': Decimal('-1.0'),
15 'max': Decimal('1.0'),
16 'med': Decimal('0.0'), 'hasIntegerValuation': True
17 }
18>>> dg.showRelationTable()
19 * ---- Relation Table -----
20 S | 'a1' 'a2' 'a3' 'a4' 'a5'
21 ------|-------------------------------
22 'a1' | -1 -1 1 -1 -1
23 'a2' | 1 -1 -1 1 1
24 'a3' | 1 -1 -1 1 -1
25 'a4' | 1 1 1 -1 -1
26 'a5' | 1 1 1 -1 -1
27 Valuation domain: [-1;+1]
28>>> dg.showComponents()
29 *--- Connected Components ---*
30 1: ['a1', 'a2', 'a3', 'a4', 'a5']
31>>> dg.showNeighborhoods()
32 Neighborhoods:
33 Gamma :
34 'a1': in => {'a2', 'a4', 'a3', 'a5'}, out => {'a3'}
35 'a2': in => {'a5', 'a4'}, out => {'a1', 'a4', 'a5'}
36 'a3': in => {'a1', 'a4', 'a5'}, out => {'a1', 'a4'}
37 'a4': in => {'a2', 'a3'}, out => {'a1', 'a3', 'a2'}
38 'a5': in => {'a2'}, out => {'a1', 'a3', 'a2'}
39 Not Gamma :
40 'a1': in => set(), out => {'a2', 'a4', 'a5'}
41 'a2': in => {'a1', 'a3'}, out => {'a3'}
42 'a3': in => {'a2'}, out => {'a2', 'a5'}
43 'a4': in => {'a1', 'a5'}, out => {'a5'}
44 'a5': in => {'a1', 'a4', 'a3'}, out => {'a4'}
```

The `exportGraphViz()`

method generates in
the current working directory a ‘tutorialDigraph.dot’ file and a
‘tutorialdigraph.png’ picture of the tutorial digraph *dg* (see Fig. 1.1), if the graphviz tools are installed on your system 1.

```
1>>> dg.exportGraphViz('tutorialDigraph')
2 *---- exporting a dot file do GraphViz tools ---------*
3 Exporting to tutorialDigraph.dot
4 dot -Grankdir=BT -Tpng tutorialDigraph.dot -o tutorialDigraph.png
```

Further methods are provided for inspecting this `Digraph`

object *dg* , like the following `showStatistics()`

method.

```
1>>> dg.showStatistics()
2 *----- general statistics -------------*
3 for digraph : <tutorialDigraph.py>
4 order : 5 nodes
5 size : 12 arcs
6 # undetermined : 0 arcs
7 determinateness (%) : 100.0
8 arc density : 0.60
9 double arc density : 0.40
10 single arc density : 0.40
11 absence density : 0.20
12 strict single arc density: 0.40
13 strict absence density : 0.20
14 # components : 1
15 # strong components : 1
16 transitivity degree (%) : 60.0
17 : [0, 1, 2, 3, 4, 5]
18 outdegrees distribution : [0, 1, 1, 3, 0, 0]
19 indegrees distribution : [0, 1, 2, 1, 1, 0]
20 mean outdegree : 2.40
21 mean indegree : 2.40
22 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
23 symmetric degrees dist. : [0, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0]
24 mean symmetric degree : 4.80
25 outdegrees concentration index : 0.1667
26 indegrees concentration index : 0.2333
27 symdegrees concentration index : 0.0333
28 : [0, 1, 2, 3, 4, 'inf']
29 neighbourhood depths distribution: [0, 1, 4, 0, 0, 0]
30 mean neighbourhood depth : 1.80
31 digraph diameter : 2
32 agglomeration distribution :
33 a1 : 58.33
34 a2 : 33.33
35 a3 : 33.33
36 a4 : 50.00
37 a5 : 50.00
38 agglomeration coefficient : 45.00
```

These *show* methods usually rely upon corresponding *compute* methods, like the `computeSize()`

, the `computeDeterminateness()`

or the `computeTransitivityDegree()`

method (see Listing 1.3 Line 5,7,16).

```
1>>> dg.computeSize()
2 12
3>>> dg.computeDeterminateness(InPercents=True)
4 Decimal('100.00')
5>>> dg.computeTransitivityDegree(InPercents=True)
6 Decimal('60.00')
```

Mind that *show* methods output their results in the Python console. We provide also some *showHTML* methods which output their results in a system browser’s window.

```
>>> dg.showHTMLRelationMap(relationName='r(x,y)',rankingRule=None)
```

In Fig. 1.2 we find confirmed again that our random digraph instance *dg*, is indeed a crisp, i.e. 100% determined digraph instance.

### 1.2.7. Special *Digraph* instances

Some constructors for universal digraph instances, like the `CompleteDigraph`

, the `EmptyDigraph`

or the *circular oriented* `GridDigraph`

constructor, are readily available (see Fig. 1.3).

```
1>>> from digraphs import GridDigraph
2>>> grid = GridDigraph(n=5,m=5,hasMedianSplitOrientation=True)
3>>> grid.exportGraphViz('tutorialGrid')
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to tutorialGrid.dot
6 dot -Grankdir=BT -Tpng TutorialGrid.dot -o tutorialGrid.png
```

For more information about its resources, see the technical documentation of the digraphs module.

Back to Content Table

## 1.3. Working with the `digraphs`

module

### 1.3.1. Random digraphs

We are starting this tutorial with generating a uniformly random [-1.0; +1.0]-valued digraph of order 7, denoted *rdg* and modelling, for instance, a binary relation (*x S y*) defined on the set of nodes of *rdg*. For this purpose, the *Digraph3* collection contains a `randomDigraphs`

module providing a specific `RandomValuationDigraph`

constructor.

```
1>>> from randomDigraphs import RandomValuationDigraph
2>>> rdg = RandomValuationDigraph(order=7)
3>>> rdg.save('tutRandValDigraph')
4>>> from digraphs import Digraph
5>>> rdg = Digraph('tutRandValDigraph')
6>>> rdg
7 *------- Digraph instance description ------*
8 Instance class : Digraph
9 Instance name : tutRandValDigraph
10 Digraph Order : 7
11 Digraph Size : 22
12 Valuation domain : [-1.00;1.00]
13 Determinateness (%) : 75.24
14 Attributes : ['name', 'actions', 'order',
15 'valuationdomain', 'relation',
16 'gamma', 'notGamma']
```

With the `save()`

method (see Listing 1.4 Line 3) we may keep a backup version for future use of *rdg* which will be stored in a file called *tutRandValDigraph.py* in the current working directory. The genuine `Digraph`

class constructor may restore the *rdg* object from the stored file (Line 4). We may easily inspect the content of *rdg* (Lines 5). The digraph size 22 indicates the number of positively valued arcs. The valuation domain is uniformly distributed in the interval and the mean absolute arc valuation is (Line 12) .

All `Digraph`

objects contain at least the list of attributes shown here: a **name** (string), a dictionary of **actions** (digraph nodes), an **order** (integer) attribute containing the number of actions, a **valuationdomain** dictionary, a double dictionary **relation** representing the adjency table of the digraph relation, a **gamma** and a **notGamma** dictionary containing the direct neighbourhood of each action.

As mentioned previously, the `Digraph`

class provides some generic *show…* methods for exploring a given *Digraph* object, like the `showShort()`

, `showAll()`

, `showRelationTable()`

and the `showNeighborhoods()`

methods.

```
1>>> rdg.showAll()
2 *----- show detail -------------*
3 Digraph : tutRandValDigraph
4 *---- Actions ----*
5 ['1', '2', '3', '4', '5', '6', '7']
6 *---- Characteristic valuation domain ----*
7 {'med': Decimal('0.0'), 'hasIntegerValuation': False,
8 'min': Decimal('-1.0'), 'max': Decimal('1.0')}
9 * ---- Relation Table -----
10 r(xSy) | '1' '2' '3' '4' '5' '6' '7'
11 -------|-------------------------------------------
12 '1' | 0.00 -0.48 0.70 0.86 0.30 0.38 0.44
13 '2' | -0.22 0.00 -0.38 0.50 0.80 -0.54 0.02
14 '3' | -0.42 0.08 0.00 0.70 -0.56 0.84 -1.00
15 '4' | 0.44 -0.40 -0.62 0.00 0.04 0.66 0.76
16 '5' | 0.32 -0.48 -0.46 0.64 0.00 -0.22 -0.52
17 '6' | -0.84 0.00 -0.40 -0.96 -0.18 0.00 -0.22
18 '7' | 0.88 0.72 0.82 0.52 -0.84 0.04 0.00
19 *--- Connected Components ---*
20 1: ['1', '2', '3', '4', '5', '6', '7']
21 Neighborhoods:
22 Gamma:
23 '1': in => {'5', '7', '4'}, out => {'5', '7', '6', '3', '4'}
24 '2': in => {'7', '3'}, out => {'5', '7', '4'}
25 '3': in => {'7', '1'}, out => {'6', '2', '4'}
26 '4': in => {'5', '7', '1', '2', '3'}, out => {'5', '7', '1', '6'}
27 '5': in => {'1', '2', '4'}, out => {'1', '4'}
28 '6': in => {'7', '1', '3', '4'}, out => set()
29 '7': in => {'1', '2', '4'}, out => {'1', '2', '3', '4', '6'}
30 Not Gamma:
31 '1': in => {'6', '2', '3'}, out => {'2'}
32 '2': in => {'5', '1', '4'}, out => {'1', '6', '3'}
33 '3': in => {'5', '6', '2', '4'}, out => {'5', '7', '1'}
34 '4': in => {'6'}, out => {'2', '3'}
35 '5': in => {'7', '6', '3'}, out => {'7', '6', '2', '3'}
36 '6': in => {'5', '2'}, out => {'5', '7', '1', '3', '4'}
37 '7': in => {'5', '6', '3'}, out => {'5'}
```

Warning

Mind that most Digraph class methods will ignore the **reflexive** links by considering that they are **indeterminate**, i.e. the characteristic value for all action *x* is set to the *median*, i.e. *indeterminate* value 0.0 in this case (see Listing 1.5 Lines 12-18 and [BIS-2004a]).

### 1.3.2. Graphviz drawings

We may even get a better insight into the `Digraph`

object *rdg* by looking at a graphviz drawing 1 .

```
1>>> rdg.exportGraphViz('tutRandValDigraph')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to tutRandValDigraph.dot
4 dot -Grankdir=BT -Tpng tutRandValDigraph.dot -o tutRandValDigraph.png
```

Double links are drawn in bold black with an arrowhead at each end, whereas single asymmetric links are drawn in black with an arrowhead showing the direction of the link. Notice the undetermined relational situation () observed between nodes ‘6’ and ‘2’. The corresponding link is marked in gray with an open arrowhead in the drawing (see Fig. 1.4).

### 1.3.3. Asymmetric and symmetric parts

We may now extract both the *symmetric* as well as the *asymmetric* part of digraph *dg* with the help of two corresponding constructors (see Fig. 1.5).

```
1>>> from digraphs import AsymmetricPartialDigraph,\
2... SymmetricPartialDigraph
3
4>>> asymDg = AsymmetricPartialDigraph(rdg)
5>>> asymDg.exportGraphViz()
6>>> symDg = SymmetricPartialDigraph(rdg)
7>>> symDg.exportGraphViz()
```

Note

The constructor of the partial objects *asymDg* and *symDg* puts to the indeterminate characteristic value all *not-asymmetric*, respectively *not-symmetric* links between nodes (see Fig. 1.5).

Here below, for illustration the source code of the *relation* constructor of the `AsymmetricPartialDigraph`

class.

```
1 def _constructRelation(self):
2 actions = self.actions
3 Min = self.valuationdomain['min']
4 Max = self.valuationdomain['max']
5 Med = self.valuationdomain['med']
6 relationIn = self.relation
7 relationOut = {}
8 for a in actions:
9 relationOut[a] = {}
10 for b in actions:
11 if a != b:
12 if relationIn[a][b] >= Med and relationIn[b][a] <= Med:
13 relationOut[a][b] = relationIn[a][b]
14 elif relationIn[a][b] <= Med and relationIn[b][a] >= Med:
15 relationOut[a][b] = relationIn[a][b]
16 else:
17 relationOut[a][b] = Med
18 else:
19 relationOut[a][b] = Med
20 return relationOut
```

### 1.3.4. Border and inner parts

We may also extract the border -the part of a digraph induced by the union of its initial and terminal prekernels (see tutorial On computing digraph kernels)- as well as, the inner part -the *complement* of the border- with the help of two corresponding class constructors: `GraphBorder`

and `GraphInner`

(see Listing 1.6).

Let us illustrate these parts on a linear ordering obtained from the tutorial random valuation digraph *rdg* with the NetFlows ranking rule (see Listing 1.6 Line 2-3).

```
1>>> from digraphs import GraphBorder, GraphInner
2>>> from linearOrders import NetFlowsOrder
3>>> nf = NetFlowsOrder(rdg)
4>>> nf.netFlowsOrder
5 ['6', '4', '5', '3', '2', '1', '7']
6>>> bnf = GraphBorder(nf)
7>>> bnf.exportGraphViz(worstChoice=['6'],bestChoice=['7'])
8>>> inf = GraphInner(nf)
9>>> inf.exportGraphViz(worstChoice=['6'],bestChoice=['7'])
```

We may orient the graphviz drawings in Fig. 1.6 with the terminal node 6 (*worstChoice* parameter) and initial node 7 (*bestChoice* parameter), see Listing 1.6 Lines 7 and 9).

Note

The constructor of the partial digraphs *bnf* and *inf* (see Listing 1.6 Lines 3 and 6) puts to the *indeterminate* characteristic value all links *not* in the *border*, respectively *not* in the *inner* part (see Fig. 1.7).

Being much *denser* than a linear order, the actual inner part of our tutorial random valuation digraph *dg* is reduced to a single arc between nodes 3 and 4 (see Fig. 1.7).

Indeed, a *complete* digraph on the limit has no inner part (privacy!) at all, whereas *empty* and *indeterminate* digraphs admit both, an empty border and an empty inner part.

### 1.3.5. Fusion by epistemic disjunction

We may recover object *rdg* from both partial objects *asymDg* and *symDg*, or as well from the border *bg* and the inner part *ig*, with a **bipolar fusion** constructor, also called **epistemic disjunction**, available via the `FusionDigraph`

class (see Listing 1.4 Lines 12- 21).

```
1>>> from digraphs import FusionDigraph
2>>> fusDg = FusionDigraph(asymDg,symDg,operator='o-max')
3>>> # fusDg = FusionDigraph(bg,ig,operator='o-max')
4>>> fusDg.showRelationTable()
5 * ---- Relation Table -----
6 r(xSy) | '1' '2' '3' '4' '5' '6' '7'
7 -------|------------------------------------------
8 '1' | 0.00 -0.48 0.70 0.86 0.30 0.38 0.44
9 '2' | -0.22 0.00 -0.38 0.50 0.80 -0.54 0.02
10 '3' | -0.42 0.08 0.00 0.70 -0.56 0.84 -1.00
11 '4' | 0.44 -0.40 -0.62 0.00 0.04 0.66 0.76
12 '5' | 0.32 -0.48 -0.46 0.64 0.00 -0.22 -0.52
13 '6' | -0.84 0.00 -0.40 -0.96 -0.18 0.00 -0.22
14 '7' | 0.88 0.72 0.82 0.52 -0.84 0.04 0.00
```

The epistemic fusion operator **o-max** (see Listing 1.7 Line 2) works as follows.

Let *r* and *r’* characterise two bipolar-valued epistemic situations.

o-max(

r,r’) = max(r,r’) when bothrandr’are more or less valid or indeterminate;o-max(

r,r’) = min(r,r’) when bothrandr’are more or less invalid or indeterminate;o-max(

r,r’) =indeterminateotherwise.

### 1.3.6. Dual, converse and codual digraphs

We may as readily compute the **dual** (negated relation 14), the **converse** (transposed relation) and the **codual** (transposed and negated relation) of the digraph instance *rdg*.

```
1>>> from digraphs import DualDigraph, ConverseDigraph, CoDualDigraph
2>>> ddg = DualDigraph(rdg)
3>>> ddg.showRelationTable()
4 -r(xSy) | '1' '2' '3' '4' '5' '6' '7'
5 --------|------------------------------------------
6 '1 ' | 0.00 0.48 -0.70 -0.86 -0.30 -0.38 -0.44
7 '2' | 0.22 0.00 0.38 -0.50 0.80 0.54 -0.02
8 '3' | 0.42 0.08 0.00 -0.70 0.56 -0.84 1.00
9 '4' | -0.44 0.40 0.62 0.00 -0.04 -0.66 -0.76
10 '5' | -0.32 0.48 0.46 -0.64 0.00 0.22 0.52
11 '6' | 0.84 0.00 0.40 0.96 0.18 0.00 0.22
12 '7' | 0.88 -0.72 -0.82 -0.52 0.84 -0.04 0.00
13>>> cdg = ConverseDigraph(rdg)
14>>> cdg.showRelationTable()
15 * ---- Relation Table -----
16 r(ySx) | '1' '2' '3' '4' '5' '6' '7'
17 --------|------------------------------------------
18 '1' | 0.00 -0.22 -0.42 0.44 0.32 -0.84 0.88
19 '2' | -0.48 0.00 0.08 -0.40 -0.48 0.00 0.72
20 '3' | 0.70 -0.38 0.00 -0.62 -0.46 -0.40 0.82
21 '4' | 0.86 0.50 0.70 0.00 0.64 -0.96 0.52
22 '5' | 0.30 0.80 -0.56 0.04 0.00 -0.18 -0.84
23 '6' | 0.38 -0.54 0.84 0.66 -0.22 0.00 0.04
24 '7' | 0.44 0.02 -1.00 0.76 -0.52 -0.22 0.00
25>>> cddg = CoDualDigraph(rdg)
26>>> cddg.showRelationTable()
27 * ---- Relation Table -----
28 -r(ySx) | '1' '2' '3' '4' '5' '6' '7'
29 --------|------------------------------------------
30 '1' | 0.00 0.22 0.42 -0.44 -0.32 0.84 -0.88
31 '2' | 0.48 0.00 -0.08 0.40 0.48 0.00 -0.72
32 '3' | -0.70 0.38 0.00 0.62 0.46 0.40 -0.82
33 '4' | -0.86 -0.50 -0.70 0.00 -0.64 0.96 -0.52
34 '5' | -0.30 -0.80 0.56 -0.04 0.00 0.18 0.84
35 '6' | -0.38 0.54 -0.84 -0.66 0.22 0.00 -0.04
36 '7' | -0.44 -0.02 1.00 -0.76 0.52 0.22 0.00
```

Computing the *dual*, respectively the *converse*, may also be done with prefixing the *__neg__* (-) or the *__invert__* (~) operator. The *codual* of a Digraph object may, hence, as well be computed with a **composition** (in either order) of both operations.

```
1>>> ddg = -rdg # dual of rdg
2>>> cdg = ~rdg # converse of rdg
3>>> cddg = ~(-rdg) # = -(~(rdg) codual of rdg
4>>> (-(~rdg)).showRelationTable()
5 * ---- Relation Table -----
6 -r(ySx) | '1' '2' '3' '4' '5' '6' '7'
7 --------|------------------------------------------
8 '1' | 0.00 0.22 0.42 -0.44 -0.32 0.84 -0.88
9 '2' | 0.48 0.00 -0.08 0.40 0.48 0.00 -0.72
10 '3' | -0.70 0.38 0.00 0.62 0.46 0.40 -0.82
11 '4' | -0.86 -0.50 -0.70 0.00 -0.64 0.96 -0.52
12 '5' | -0.30 -0.80 0.56 -0.04 0.00 0.18 0.84
13 '6' | -0.38 0.54 -0.84 -0.66 0.22 0.00 -0.04
14 '7' | -0.44 -0.02 1.00 -0.76 0.52 0.22 0.00
```

### 1.3.7. Symmetric and transitive closures

Symmetric and transitive closures, by default in-site constructors, are also available (see Fig. 1.8). Note that it is a good idea, before going ahead with these in-site operations, who irreversibly modify the original *rdg* object, to previously make a backup version of *rdg*. The simplest storage method, always provided by the generic `save()`

, writes out in a named file the python content of the Digraph object in string representation.

```
1>>> rdg.save('tutRandValDigraph')
2>>> rdg.closeSymmetric(InSite=True)
3>>> rdg.closeTransitive(InSite=True)
4>>> rdg.exportGraphViz('strongComponents')
```

The `closeSymmetric()`

method (see Listing 1.9 Line 2), of complexity where *n* denotes the digraph’s order, changes, on the one hand, all single pairwise links it may detect into double links by operating a disjunction of the pairwise relations. On the other hand, the `closeTransitive()`

method (see Listing 1.9 Line 3), implements the *Roy-Warshall* transitive closure algorithm of complexity . (17)

Note

The same `closeTransitive()`

method with a *Reverse = True* flag may be readily used for eliminating all transitive arcs from a transitive digraph instance. We make usage of this feature when drawing *Hasse diagrams* of `TransitiveDigraph`

objects.

### 1.3.8. Strong components

As the original digraph *rdg* was connected (see above the result of the `showShort()`

command), both the symmetric and the transitive closures operated together, will necessarily produce a single strong component, i.e. a **complete** digraph. We may sometimes wish to collapse all strong components in a given digraph and construct the so *collapsed* digraph. Using the `StrongComponentsCollapsedDigraph`

constructor here will render a single hyper-node gathering all the original nodes (see Line 7 below).

```
1>>> from digraphs import StrongComponentsCollapsedDigraph
2>>> sc = StrongComponentsCollapsedDigraph(dg)
3>>> sc.showAll()
4 *----- show detail -----*
5 Digraph : tutRandValDigraph_Scc
6 *---- Actions ----*
7 ['_7_1_2_6_5_3_4_']
8 * ---- Relation Table -----
9 S | 'Scc_1'
10 -------|---------
11 'Scc_1' | 0.00
12 short content
13 Scc_1 _7_1_2_6_5_3_4_
14 Neighborhoods:
15 Gamma :
16 'frozenset({'7', '1', '2', '6', '5', '3', '4'})': in => set(), out => set()
17 Not Gamma :
18 'frozenset({'7', '1', '2', '6', '5', '3', '4'})': in => set(), out => set()
```

### 1.3.9. CSV storage

Sometimes it is required to exchange the graph valuation data in CSV format with a statistical package like R. For this purpose it is possible to export the digraph data into a CSV file. The valuation domain is hereby normalized by default to the range [-1,1] and the diagonal put by default to the minimal value -1.

```
1>>> rdg = Digraph('tutRandValDigraph')
2>>> rdg.saveCSV('tutRandValDigraph')
3 # content of file tutRandValDigraph.csv
4 "d","1","2","3","4","5","6","7"
5 "1",-1.0,0.48,-0.7,-0.86,-0.3,-0.38,-0.44
6 "2",0.22,-1.0,0.38,-0.5,-0.8,0.54,-0.02
7 "3",0.42,-0.08,-1.0,-0.7,0.56,-0.84,1.0
8 "4",-0.44,0.4,0.62,-1.0,-0.04,-0.66,-0.76
9 "5",-0.32,0.48,0.46,-0.64,-1.0,0.22,0.52
10 "6",0.84,0.0,0.4,0.96,0.18,-1.0,0.22
11 "7",-0.88,-0.72,-0.82,-0.52,0.84,-0.04,-1.0
```

It is possible to reload a Digraph instance from its previously saved CSV file content.

```
1>>> from digraphs import CSVDigraph
2>>> rdgcsv = CSVDigraph('tutRandValDigraph')
3>>> rdgcsv.showRelationTable(ReflexiveTerms=False)
4 * ---- Relation Table -----
5 r(xSy) | '1' '2' '3' '4' '5' '6' '7'
6 -------|------------------------------------------------------------
7 '1' | - -0.48 0.70 0.86 0.30 0.38 0.44
8 '2' | -0.22 - -0.38 0.50 0.80 -0.54 0.02
9 '3' | -0.42 0.08 - 0.70 -0.56 0.84 -1.00
10 '4' | 0.44 -0.40 -0.62 - 0.04 0.66 0.76
11 '5' | 0.32 -0.48 -0.46 0.64 - -0.22 -0.52
12 '6' | -0.84 0.00 -0.40 -0.96 -0.18 - -0.22
13 '7' | 0.88 0.72 0.82 0.52 -0.84 0.04 -
```

It is as well possible to show a colored version of the valued relation table in a system browser window tab (see Fig. 1.9).

```
1>>> rdgcsv.showHTMLRelationTable(tableTitle="Tutorial random digraph")
```

Positive arcs are shown in *green* and negative arcs in *red*. Indeterminate -zero-valued- links, like the reflexive diagonal ones or the link between node *6* and node *2*, are shown in *gray*.

### 1.3.10. Complete, empty and indeterminate digraphs

Let us finally mention some special universal classes of digraphs that are readily available in the `digraphs`

module, like the `CompleteDigraph`

, the `EmptyDigraph`

and the `IndeterminateDigraph`

classes, which put all characteristic values respectively to the *maximum*, the *minimum* or the median *indeterminate* characteristic value.

```
1>>> from digraphs import CompleteDigraph,EmptyDigraph,\
2... IndeterminateDigraph
3
4>>> e = EmptyDigraph(order=5)
5>>> e.showRelationTable()
6 * ---- Relation Table -----
7 S | '1' '2' '3' '4' '5'
8 ---- -|-----------------------------------
9 '1' | -1.00 -1.00 -1.00 -1.00 -1.00
10 '2' | -1.00 -1.00 -1.00 -1.00 -1.00
11 '3' | -1.00 -1.00 -1.00 -1.00 -1.00
12 '4' | -1.00 -1.00 -1.00 -1.00 -1.00
13 '5' | -1.00 -1.00 -1.00 -1.00 -1.00
14 >>> e.showNeighborhoods()
15 Neighborhoods:
16 Gamma :
17 '1': in => set(), out => set()
18 '2': in => set(), out => set()
19 '5': in => set(), out => set()
20 '3': in => set(), out => set()
21 '4': in => set(), out => set()
22 Not Gamma :
23 '1': in => {'2', '4', '5', '3'}, out => {'2', '4', '5', '3'}
24 '2': in => {'1', '4', '5', '3'}, out => {'1', '4', '5', '3'}
25 '5': in => {'1', '2', '4', '3'}, out => {'1', '2', '4', '3'}
26 '3': in => {'1', '2', '4', '5'}, out => {'1', '2', '4', '5'}
27 '4': in => {'1', '2', '5', '3'}, out => {'1', '2', '5', '3'}
28>>> i = IndeterminateDigraph()
29 * ---- Relation Table -----
30 S | '1' '2' '3' '4' '5'
31 ------|------------------------------
32 '1' | 0.00 0.00 0.00 0.00 0.00
33 '2' | 0.00 0.00 0.00 0.00 0.00
34 '3' | 0.00 0.00 0.00 0.00 0.00
35 '4' | 0.00 0.00 0.00 0.00 0.00
36 '5' | 0.00 0.00 0.00 0.00 0.00
37>>> i.showNeighborhoods()
38 Neighborhoods:
39 Gamma :
40 '1': in => set(), out => set()
41 '2': in => set(), out => set()
42 '5': in => set(), out => set()
43 '3': in => set(), out => set()
44 '4': in => set(), out => set()
45 Not Gamma :
46 '1': in => set(), out => set()
47 '2': in => set(), out => set()
48 '5': in => set(), out => set()
49 '3': in => set(), out => set()
50 '4': in => set(), out => set()
```

Note

Mind the subtle difference between the neighborhoods of an **empty** and the neighborhoods of an **indeterminate** digraph instance. In the first kind, the neighborhoods are known to be completely *empty* (see Listing 1.10 Lines 22-27) whereas, in the latter, *nothing is known* about the actual neighborhoods of the nodes (see Listing 1.10 Lines 45-50). These two cases illustrate why in the case of **bipolar-valued** digraphs, we may need both a *gamma* **and** a *notGamma* attribute.

Back to Content Table

## 1.4. Working with the `outrankingDigraphs`

module

See also

The technical documentation of the outrankingDigraphs module.

### 1.4.1. Outranking digraph model

In this *Digraph3* module, the `BipolarOutrankingDigraph`

class from the `outrankingDigraphs`

mudule provides our standard **outranking digraph** constructor. Such an instance represents a **hybrid** object of both, the `PerformanceTableau`

type and the `OutrankingDigraph`

type. A given object consists hence in:

an ordered dictionary of decision

actionsdescribing the potential decision actions or alternatives with ‘name’ and ‘comment’ attributes,a possibly empty ordered dictionary of decision

objectiveswith ‘name’ and ‘comment attributes, describing the multiple preference dimensions involved in the decision problem,a dictionary of performance

criteriadescribingpreferentially independentandnon-redundantdecimal-valued functions used for measuring the performance of each potential decision action with respect to a decision objective,a double dictionary

evaluationgathering performance grades for each decision action or alternative on each criterion function.the digraph

valuationdomain, a dictionary with three entries: theminimum(-1.0, certainly outranked), themedian(0.0, indeterminate) and themaximumcharacteristic value (+1.0, certainly outranking),the outranking

relation: a double dictionary defined on the Cartesian product of the set of decision alternatives capturing the credibility of the pairwiseoutranking situationcomputed on the basis of the performance differences observed between couples of decision alternatives on the given family if criteria functions.

Let us construct, for instance, a random bipolar-valued outranking digraph with seven decision actions denotes *a1*, *a2*, …, *a7*. We need therefore to first generate a corresponding random performance tableaux (see below).

```
1>>> from outrankingDigraphs import *
2>>> pt = RandomPerformanceTableau(numberOfActions=7,\
3... seed=100)
4
5>>> pt
6*------- PerformanceTableau instance description ------*
7 Instance class : RandomPerformanceTableau
8 Seed : 100
9 Instance name : randomperftab
10 # Actions : 7
11 # Criteria : 7
12 NaN proportion (%) : 6.1
13>>> pt.showActions()
14 *----- show digraphs actions --------------*
15 key: a1
16 name: action #1
17 comment: RandomPerformanceTableau() generated.
18 key: a2
19 name: action #2
20 comment: RandomPerformanceTableau() generated.
21 ...
22 ...
23 key: a7
24 name: action #7
25 comment: RandomPerformanceTableau() generated.
```

In this example we consider furthermore a family of seven **equisignificant cardinal criteria functions** *g1*, *g2*, …, *g7*, measuring the performance of each alternative on a rational scale from 0.0 (worst) to 100.00 (best). In order to capture the grading procedure’s potential uncertainty and imprecision, each criterion function *g1* to *g7* admits three performance **discrimination thresholds** of 2.5, 5.0 and 80 pts for warranting respectively any indifference, preference or considerable performance difference situation.

```
1>>> pt.showCriteria()
2 *---- criteria -----*
3 g1 'RandomPerformanceTableau() instance'
4 Scale = [0.0, 100.0]
5 Weight = 1.0
6 Threshold ind : 2.50 + 0.00x ; percentile: 4.76
7 Threshold pref : 5.00 + 0.00x ; percentile: 9.52
8 Threshold veto : 80.00 + 0.00x ; percentile: 95.24
9 g2 'RandomPerformanceTableau() instance'
10 Scale = [0.0, 100.0]
11 Weight = 1.0
12 Threshold ind : 2.50 + 0.00x ; percentile: 6.67
13 Threshold pref : 5.00 + 0.00x ; percentile: 6.67
14 Threshold veto : 80.00 + 0.00x ; percentile: 100.00
15 ...
16 ...
17 g7 'RandomPerformanceTableau() instance'
18 Scale = [0.0, 100.0]
19 Weight = 1.0
20 Threshold ind : 2.50 + 0.00x ; percentile: 0.00
21 Threshold pref : 5.00 + 0.00x ; percentile: 4.76
22 Threshold veto : 80.00 + 0.00x ; percentile: 100.00
```

On criteria function *g1* (see Lines 6-8 above) we observe, for instance, about 5% of **indifference**, about 90% of **preference** and about 5% of **considerable** performance difference situations. The individual performance evaluation of all decision alternative on each criterion are gathered in a *performance tableau*.

```
1>>> pt.showPerformanceTableau()
2 *---- performance tableau -----*
3 criteria | 'a1' 'a2' 'a3' 'a4' 'a5' 'a6' 'a7'
4 ---------|------------------------------------------
5 'g1' | 15.2 44.5 57.9 58.0 24.2 29.1 96.6
6 'g2' | 82.3 43.9 NA 35.8 29.1 34.8 62.2
7 'g3' | 44.2 19.1 27.7 41.5 22.4 21.5 56.9
8 'g4' | 46.4 16.2 21.5 51.2 77.0 39.4 32.1
9 'g5' | 47.7 14.8 79.7 67.5 NA 90.7 80.2
10 'g6' | 69.6 45.5 22.0 33.8 31.8 NA 48.8
11 'g7' | 82.9 41.7 12.8 21.9 75.7 15.4 6.0
```

It is noteworthy to mention the three **missing data** (*NA*) cases: action *a3* is missing, for instance, a grade on criterion *g2* (see Line 6 above).

### 1.4.2. The bipolar-valued outranking digraph

Given the previous random performance tableau *pt*, the `BipolarOutrankingDigraph`

constructor computes the corresponding **bipolar-valued outranking digraph**.

```
1>>> odg = BipolarOutrankingDigraph(pt)
2>>> odg
3 *------- Object instance description ------*
4 Instance class : BipolarOutrankingDigraph
5 Instance name : rel_randomperftab
6 # Actions : 7
7 # Criteria : 7
8 Size : 20
9 Determinateness (%) : 63.27
10 Valuation domain : [-1.00;1.00]
11 Attributes : [
12 'name', 'actions',
13 'criteria', 'evaluation', 'NA',
14 'valuationdomain', 'relation',
15 'order', 'gamma', 'notGamma', ...
16 ]
```

The resulting digraph contains 20 positive (valid) outranking realtions. And, the mean majority criteria significance support of all the pairwise outranking situations is 63.3% (see Listing 1.11 Lines 8-9). We may inspect the complete [-1.0,+1.0]-valued adjacency table as follows.

```
1>>> odg.showRelationTable()
2 * ---- Relation Table -----
3 r(x,y)| 'a1' 'a2' 'a3' 'a4' 'a5' 'a6' 'a7'
4 ------|-------------------------------------------------
5 'a1' | +1.00 +0.71 +0.29 +0.29 +0.29 +0.29 +0.00
6 'a2' | -0.71 +1.00 -0.29 -0.14 +0.14 +0.29 -0.57
7 'a3' | -0.29 +0.29 +1.00 -0.29 -0.14 +0.00 -0.29
8 'a4' | +0.00 +0.14 +0.57 +1.00 +0.29 +0.57 -0.43
9 'a5' | -0.29 +0.00 +0.14 +0.00 +1.00 +0.29 -0.29
10 'a6' | -0.29 +0.00 +0.14 -0.29 +0.14 +1.00 +0.00
11 'a7' | +0.00 +0.71 +0.57 +0.43 +0.29 +0.00 +1.00
12 Valuation domain: [-1.0; 1.0]
```

Considering the given performance tableau *pt*, the `BipolarOutrankingDigraph`

class constructor computes the characteristic value of a **pairwise outranking** relation “” (see [BIS-2013], [ADT-L7]) in a default *normalised* **valuation domain** [-1.0,+1.0] with the *median value* 0.0 acting as **indeterminate** characteristic value. The semantics of are the following.

When , it is more

TruethanFalsethatxoutranksy, i.e. alternativexis at least as well performing than alternativeyon a weighted majority of criteriaandthere is no considerable negative performance difference observed in disfavour ofx,When , it is more

FalsethanTruethatxoutranksy, i.e. alternativexisnotat least as well performing on a weighted majority of criteria than alternativeyandthere is no considerable positive performance difference observed in favour ofx,When , it is

indeterminatewhetherxoutranksyor not.

### 1.4.3. Pairwise comparisons

From above given semantics, we may consider (see Line 5 above) that *a1* outranks *a2* (), but not *a7* (). In order to comprehend the characteristic values shown in the relation table above, we may furthermore inspect the details of the pairwise multiple criteria comparison between alternatives *a1* and *a2*.

```
1>>> odg.showPairwiseComparison('a1','a2')
2 *------------ pairwise comparison ----*
3 Comparing actions : (a1, a2)
4 crit. wght. g(x) g(y) diff | ind pref r()
5 ------------------------------- --------------------
6 g1 1.00 15.17 44.51 -29.34 | 2.50 5.00 -1.00
7 g2 1.00 82.29 43.90 +38.39 | 2.50 5.00 +1.00
8 g3 1.00 44.23 19.10 +25.13 | 2.50 5.00 +1.00
9 g4 1.00 46.37 16.22 +30.15 | 2.50 5.00 +1.00
10 g5 1.00 47.67 14.81 +32.86 | 2.50 5.00 +1.00
11 g6 1.00 69.62 45.49 +24.13 | 2.50 5.00 +1.00
12 g7 1.00 82.88 41.66 +41.22 | 2.50 5.00 +1.00
13 ----------------------------------------
14 Valuation in range: -7.00 to +7.00; r(x,y): +5/7 = +0.71
```

The outranking characteristic value represents the **majority margin** resulting from the difference between the weights of the criteria in favor and the weights of the criteria in disfavor of the statement that alternative *a1* is at least as well performing as alternative *a2*. No considerable performance difference being observed above, no veto or counter-veto situation is triggered in this pairwise comparison. Such a situation is, however, observed for instance when we pairwise compare the performances of alternatives *a1* and *a7*.

```
1>>> odg.showPairwiseComparison('a1','a7')
2 *------------ pairwise comparison ----*
3 Comparing actions : (a1, a7)
4 crit. wght. g(x) g(y) diff | ind pref r() | v veto
5 ------------------------------- ------------------ -----------
6 g1 1.00 15.17 96.58 -81.41 | 2.50 5.00 -1.00 | 80.00 -1.00
7 g2 1.00 82.29 62.22 +20.07 | 2.50 5.00 +1.00 |
8 g3 1.00 44.23 56.90 -12.67 | 2.50 5.00 -1.00 |
9 g4 1.00 46.37 32.06 +14.31 | 2.50 5.00 +1.00 |
10 g5 1.00 47.67 80.16 -32.49 | 2.50 5.00 -1.00 |
11 g6 1.00 69.62 48.80 +20.82 | 2.50 5.00 +1.00 |
12 g7 1.00 82.88 6.05 +76.83 | 2.50 5.00 +1.00 |
13 ----------------------------------------
14 Valuation in range: -7.00 to +7.00; r(x,y)= +1/7 => 0.0
```

This time, we observe a 57.1% majority of criteria significance [(1/7 + 1)/2 = 0.571] warranting an *as well as performing* situation. Yet, we also observe a considerable negative performance difference on criterion *g1* (see first row in the relation table above). Both contradictory facts trigger eventually an *indeterminate* outranking situation [BIS-2013].

### 1.4.4. Recoding the digraph valuation

All outranking digraphs, being of root type `Digraph`

, inherit the methods available under this latter class. The characteristic valuation domain of a digraph may, for instance, be recoded with the `recodeValutaion()`

method below to the *integer* range [-7,+7], i.e. plus or minus the global significance of the family of criteria considered in this example instance.

```
1>>> odg.recodeValuation(-37,+37)
2>>> odg.valuationdomain['hasIntegerValuation'] = True
3>>> Digraph.showRelationTable(odg,ReflexiveTerms=False)
4 * ---- Relation Table -----
5 r(x,y) | 'a1' 'a2' 'a3' 'a4' 'a5' 'a6' 'a7'
6 ---------|------------------------------------------
7 'a1' | 0 5 2 2 2 2 0
8 'a2' | -5 0 -1 -1 1 2 -4
9 'a3' | -1 2 0 -1 -1 0 -1
10 'a4' | 0 1 4 0 2 4 -3
11 'a5' | -1 0 1 0 0 2 -1
12 'a6' | -1 0 1 -1 1 0 0
13 'a7' | 0 5 4 3 2 0 0
14 Valuation domain: [-7;+7]
```

Warning

Notice that the reflexive self comparison characteristic is set above by default to the median indeterminate valuation value 0; the reflexive terms of binary relation being generally ignored in most of the *Digraph3* resources.

### 1.4.5. The strict outranking digraph

From the theory (see [BIS-2013], [ADT-L7] ) we know that a bipolar-valued outranking digraph is **weakly complete**, i.e. if then . A bipolar-valued outranking relation verifies furthermore the **coduality** principle: the **dual** (*strict negation* - 14) of the **converse** (*inverse* ~) of the outranking relation corresponds to its *strict outranking* part.

We may visualize the **codual** (*strict*) outranking digraph with a graphviz drawing 1.

```
1>>> cdodg = -(~odg)
2>>> cdodg.exportGraphViz('codualOdg')
3 *---- exporting a dot file for GraphViz tools ---------*
4 Exporting to codualOdg.dot
5 dot -Grankdir=BT -Tpng codualOdg.dot -o codualOdg.png
```

It becomes readily clear now from the picture above that both alternatives *a1* and *a07* are *not outranked* by any other alternatives. Hence, *a1* and *a7* appear as **weak Condorcet winner** and may be recommended as potential *best decision actions* in this illustrative preference modelling exercise.

Many more tools for exploiting bipolar-valued outranking digraphs are available in the *Digraph3* resources (see the technical documentation of the outrankingDigraphs module and the perfTabs module).

In this tutorial we have constructed a random outranking digraph with the help of a random performance tableau instance. The next *Digraph3* tutorial presents now different models of random performance tableaux illustrating various types of decision problems.

Back to Content Table

## 1.5. Generating random performance tableaux with the `randPerfTabs`

module

### 1.5.1. Introduction

The `randomPerfTabs`

module provides several constructors for generating random performance tableaux models of different kind, mainly for the purpose of testing implemented methods and tools presented and discussed in the Algorithmic Decision Theory course at the University of Luxembourg. This tutorial concerns the most useful models.

The simplest model, called **RandomPerformanceTableau**, generates
a set of *n* decision actions, a set of *m* real-valued
performance criteria, ranging by default from 0.0 to 100.0,
associated with default discrimination thresholds: 2.5 (ind.),
5.0 (pref.) and 60.0 (veto). The generated performances are
Beta(2.2) distributed on each measurement scale.

One of the most useful models, called
**RandomCBPerformanceTableau**, proposes a performance tableau
involving two decision objectives,
named *Costs* (to be minimized) respectively *Benefits* (to be
maximized); its purpose being to generate more or less
contradictory performances on these two, usually conflicting,
objectives. *Low costs* will randomly be coupled with *low
benefits*, whereas *high costs* will randomly be coupled
with high benefits.

Many public policy decision problems involve three often
conflicting decision objectives taking into account *economical*,
*societal* as well as *environmental* aspects. For this type of
performance tableau model, we provide a specific model,
called **Random3ObjectivesPerformanceTableau**.

Deciding which students, based on the grades obtained in a
number of examinations, validate or not their academic studies,
is the genuine decision practice of universities and academies.
To thouroughly study these kind of decision problems,
we provide a corresponding performance tableau model, called
**RandomAcademicPerformanceTableau**, which gathers grades
obtained by a given number of students in a given number of
weighted courses.

In order to study aggregation of election results (see the tutorial on
Computing the winner of an election with the votingProfiles module) in the context
of bipolar-valued outranking digraphs, we provide furthermore a
specific performance tableau model called **RandomRankPerformanceTableau**
which provides ranks (linearly ordered performances without ties) of a given number of election candidates (decision actions) for a given number of weighted voters (performance criteria).

### 1.5.2. Random standard performance tableaux

The `RandomPerformanceTableau`

class, the simplest of the kind, specializes the generic `PerformanceTableau`

class, and takes the following parameters.

numberOfActions := nbr of decision actions.

numberOfCriteria := number performance criteria.

weightDistribution := ‘random’ (default) | ‘fixed’ | ‘equisignificant’:

If ‘random’, weights are uniformly selected randomlyfrom the given weight scale;If ‘fixed’, the weightScale must provided a corresponding weightsdistribution;If ‘equisignificant’, all criterion weights are put to unity.weightScale := [Min,Max] (default =(1,numberOfCriteria).

IntegerWeights := True (default) | False (normalized to proportions of 1.0).

commonScale := [a,b]; common performance measuring scales (default = [0.0,100.0])

commonThresholds := [(q0,q1),(p0,p1),(v0,v1)]; common indifference(q), preference (p) and considerable performance difference discrimination thresholds. For each threshold type

xin{q,p,v}, the float x0 value represents a constant percentage of the common scale and the float x1 value a proportional value of the actual performance measure. Default values are [(2.5.0,0.0),(5.0,0.0),(60.0,0,0)].commonMode := common random distribution of random performance measurements (default = (‘beta’,None,(2,2)) ):

(‘uniform’,None,None), uniformly distributed float values on the given common scales’ range [Min,Max].(‘normal’,*mu*,*sigma*), truncated Gaussian distribution, by defaultmu= (b-a)/2 andsigma= (b-a)/4.(‘triangular’,*mode*,*repartition*), generalized triangular distribution with a probability repartition parameter specifying the probability mass accumulated until the mode value. By default,mode= (b-a)/2 andrepartition= 0.5.(‘beta’,None,(alpha,beta)), a beta generator with default alpha=2 and beta=2 parameters.valueDigits := <integer>, precision of performance measurements (2 decimal digits by default).

missingDataProbability := 0 <= float <= 1.0 ; probability of missing performance evaluation on a criterion for an alternative (default 0.025).

NA := <Decimal> (default = -999); missing data symbol.

Code example.

```
1>>> from randomPerfTabs import RandomPerformanceTableau
2>>> t = RandomPerformanceTableau(numberOfActions=21,numberOfCriteria=13,seed=100)
3>>> t.actions
4 {'a01': {'comment': 'RandomPerformanceTableau() generated.',
5 'name': 'random decision action'},
6 'a02': { ... },
7 ...
8 }
9>>> t.criteria
10 {'g01': {'thresholds': {'ind' : (Decimal('10.0'), Decimal('0.0')),
11 'veto': (Decimal('80.0'), Decimal('0.0')),
12 'pref': (Decimal('20.0'), Decimal('0.0'))},
13 'scale': [0.0, 100.0],
14 'weight': Decimal('1'),
15 'name': 'digraphs.RandomPerformanceTableau() instance',
16 'comment': 'Arguments: ; weightDistribution=random;
17 weightScale=(1, 1); commonMode=None'},
18 'g02': { ... },
19 ...
20 }
21>>> t.evaluation
22 {'g01': {'a01': Decimal('15.17'),
23 'a02': Decimal('44.51'),
24 'a03': Decimal('-999'), # missing evaluation
25 ...
26 },
27 ...
28 }
29>>> t.showHTMLPerformanceTableau()
```

Note

Missing (NA) evaluation are registered in a performance tableau by default as *Decimal(‘-999’)* value (see Listing 1.12 Line 24). Best and worst performance on each criterion are marked in *light green*, respectively in *light red*.

### 1.5.3. Random Cost-Benefit performance tableaux

We provide the `RandomCBPerformanceTableau`

class for generating random *Cost* versus *Benefit* organized performance tableaux following the directives below:

We distinguish three types of decision actions:

cheap,neutralandexpensiveones with an equal proportion of 1/3. We also distinguish two types of weighted criteria:costcriteria to beminimized, andbenefitcriteria to bemaximized; in the proportions 1/3 respectively 2/3.Random performances on each type of criteria are drawn, either from an ordinal scale [0;10], or from a cardinal scale [0.0;100.0], following a parametric triangular law of mode: 30% performance for cheap, 50% for neutral, and 70% performance for expensive decision actions, with constant probability repartition 0.5 on each side of the respective mode.

Cost criteria use mostly cardinal scales (3/4), whereas benefit criteria use mostly ordinal scales (2/3).

The sum of weights of the cost criteria by default equals the sum weights of the benefit criteria: weighDistribution = ‘equiobjectives’.

On cardinal criteria, both of cost or of benefit type, we observe following constant preference discrimination quantiles: 5% indifferent situations, 90% strict preference situations, and 5% veto situation.

*Parameters*:If

*numberOfActions*== None, a uniform random number between 10 and 31 of cheap, neutral or advantageous actions (equal 1/3 probability each type) actions is instantiatedIf

*numberOfCriteria*== None, a uniform random number between 5 and 21 of cost or benefit criteria (1/3 respectively 2/3 probability) is instantiated*weightDistribution*= {‘equiobjectives’|’fixed’|’random’|’equisignificant’ (default = ‘equisignificant’)}default

*weightScale*for ‘random’ weightDistribution is 1 - numberOfCriteriaAll cardinal criteria are evaluated with decimals between 0.0 and 100.0 whereas ordinal criteria are evaluated with integers between 0 and 10.

commonThresholds is obsolete. Preference discrimination is specified as percentiles of concerned performance differences (see below).

commonPercentiles = {‘ind’:5, ‘pref’:10, [‘weakveto’:90,] ‘veto’:95} are expressed in percents (reversed for vetoes) and only concern cardinal criteria.

missingDataProbability := 0 <= float <= 1.0 ; probability of missing performance evaluation on a criterion for an alternative (default 0.025).

NA := <Decimal> (default = -999); missing data symbol.

Warning

Minimal number of decision actions required is 3 !

Example Python session

```
1>>> from randomPerfTabs import RandomCBPerformanceTableau
2>>> t = RandomCBPerformanceTableau(
3... numberOfActions=7,\
4... numberOfCriteria=5,\
5... weightDistribution='equiobjectives',\
6... commonPercentiles={'ind':0.05,'pref':0.10,'veto':0.95},\
7... seed=100)
8
9>>> t.showActions()
10 *----- show decision action --------------*
11 key: a1
12 short name: a1
13 name: random cheap decision action
14 key: a2
15 short name: a2
16 name: random neutral decision action
17 ...
18 key: a7
19 short name: a7
20 name: random advantageous decision action
21>>> t.showCriteria()
22 *---- criteria -----*
23 g1 'random ordinal benefit criterion'
24 Scale = (0, 10)
25 Weight = 2
26 ...
27 g2 'random cardinal cost criterion'
28 Scale = (0.0, 100.0)
29 Weight = 3
30 Threshold ind : 1.76 + 0.00x ; percentile: 9.5
31 Threshold pref : 2.16 + 0.00x ; percentile: 14.3
32 Threshold veto : 73.19 + 0.00x ; percentile: 95.2
33 ...
```

In the example above, we may notice the three types of decision actions (Listing 1.13 Lines 10-20), as well as the two types (Lines 22-32) of criteria with either an **ordinal** or a **cardinal** performance measuring scale. In the latter case, by default about 5% of the random performance differences will be below the **indifference** and 10% below the **preference discriminating threshold**. About 5% will be considered as **considerably large**. More statistics about the generated performances is available as follows.

```
1>>> t.showStatistics()
2 *-------- Performance tableau summary statistics -------*
3 Instance name : randomCBperftab
4 #Actions : 7
5 #Criteria : 5
6 Criterion name : g1
7 Criterion weight : 2
8 criterion scale : 0.00 - 10.00
9 mean evaluation : 5.14
10 standard deviation : 2.64
11 maximal evaluation : 8.00
12 quantile Q3 (x_75) : 8.00
13 median evaluation : 6.50
14 quantile Q1 (x_25) : 3.50
15 minimal evaluation : 1.00
16 mean absolute difference : 2.94
17 standard difference deviation : 3.74
18 Criterion name : g2
19 Criterion weight : 3
20 criterion scale : -100.00 - 0.00
21 mean evaluation : -49.32
22 standard deviation : 27.59
23 maximal evaluation : 0.00
24 quantile Q3 (x_75) : -27.51
25 median evaluation : -35.98
26 quantile Q1 (x_25) : -54.02
27 minimal evaluation : -91.87
28 mean absolute difference : 28.72
29 standard difference deviation : 39.02
30 ...
```

A (potentially ranked) colored heatmap with 5 color levels is also provided.

```
>>> t.showHTMLPerformanceHeatmap(colorLevels=5,rankingRule=None)
```

Such a performance tableau may be stored and re-accessed as follows.

```
1>>> t.save('temp')
2 *----- saving performance tableau in XMCDA 2.0 format -------------*
3 File: temp.py saved !
4>>> from perfTabs import PerformanceTableau
5>>> t = PerformanceTableau('temp')
```

If needed for instance in an R session, a CSV version of the performance tableau may be created as follows.

```
1>>> t.saveCSV('temp')
2 * --- Storing performance tableau in CSV format in file temp.csv
```

```
1...$ less temp.csv
2 "actions","g1","g2","g3","g4","g5"
3 "a1",1.00,-17.92,-33.99,26.68,3.00
4 "a2",8.00,-30.71,-77.77,66.35,6.00
5 "a3",8.00,-41.65,-69.84,53.43,8.00
6 "a4",2.00,-39.49,-16.99,18.62,2.00
7 "a5",6.00,-91.87,-74.85,83.09,7.00
8 "a6",7.00,-32.47,-24.91,79.24,9.00
9 "a7",4.00,-91.11,-7.44,48.22,7.00
```

Back to Content Table

### 1.5.4. Random three objectives performance tableaux

We provide the `Random3ObjectivesPerformanceTableau`

class for generating random performance tableaux concerning potential public policies evaluated with respect to three preferential decision objectives taking respectively into account *economical*, *societal* as well as *environmental* aspects.

Each public policy is qualified randomly as performing **weak** (-), **fair** (~) or **good** (+) on each of the three objectives.

Generator directives are the following:

numberOfActions = 20 (default),

numberOfCriteria = 13 (default),

weightDistribution = ‘equiobjectives’ (default) | ‘random’ | ‘equisignificant’,

weightScale = (1,numberOfCriteria): only used when random criterion weights are requested,

integerWeights = True (default): False gives normalized rational weights,

commonScale = (0.0,100.0),

commonThresholds = [(5.0,0.0),(10.0,0.0),(60.0,0.0)]: Performance discrimination thresholds may be set for ‘ind’, ‘pref’ and ‘veto’,

commonMode = [‘triangular’,’variable’,0.5]: random number generators of various other types (‘uniform’,’beta’) are available,

valueDigits = 2 (default): evaluations are encoded as Decimals,

missingDataProbability = 0.05 (default): random insertion of missing values with given probability,

NA := <Decimal> (default = -999); missing data symbol.

seed= None.

Note

If the mode of the **triangular** distribution is set to ‘*variable*’,
three modes at 0.3 (-), 0.5 (~), respectively 0.7 (+) of the common scale span are set at random for each coalition and action.

Warning

Minimal number of decision actions required is 3 !

Example Python session

```
1>>> from randomPerfTabs import Random3ObjectivesPerformanceTableau
2>>> t = Random3ObjectivesPerformanceTableau(\
3... numberOfActions=31,\
4... numberOfCriteria=13,\
5... weightDistribution='equiobjectives',\
6... seed=120)
7
8>>> t.showObjectives()
9 *------ show objectives -------"
10 Eco: Economical aspect
11 ec04 criterion of objective Eco 20
12 ec05 criterion of objective Eco 20
13 ec08 criterion of objective Eco 20
14 ec11 criterion of objective Eco 20
15 Total weight: 80.00 (4 criteria)
16 Soc: Societal aspect
17 so06 criterion of objective Soc 16
18 so07 criterion of objective Soc 16
19 so09 criterion of objective Soc 16
20 s010 criterion of objective Soc 16
21 s013 criterion of objective Soc 16
22 Total weight: 80.00 (5 criteria)
23 Env: Environmental aspect
24 en01 criterion of objective Env 20
25 en02 criterion of objective Env 20
26 en03 criterion of objective Env 20
27 en12 criterion of objective Env 20
28 Total weight: 80.00 (4 criteria)
```

In Listing 1.14 above, we notice that 5 *equisignificant* criteria (g06, g07, g09, g10, g13) evaluate for instance the performance of the public policies from a **societal** point of view (Lines 16-22). 4 *equisignificant* criteria do the same from an **economical** (Lines 10-15), respectively an **environmental** point of view (Lines 23-28). The *equiobjectives* directive results hence in a balanced total weight (80.00) for each decision objective.

```
1>>> t.showActions()
2 key: p01
3 name: random public policy Eco+ Soc- Env+
4 profile: {'Eco': 'good', 'Soc': 'weak', 'Env': 'good'}
5 key: p02
6 ...
7 key: p26
8 name: random public policy Eco+ Soc+ Env-
9 profile: {'Eco': 'good', 'Soc': 'good', 'Env': 'weak'}
10 ...
11 key: p30
12 name: random public policy Eco- Soc- Env-
13 profile: {'Eco': 'weak', 'Soc': 'weak', 'Env': 'weak'}
14 ...
```

Variable triangular modes (0.3, 0.5 or 0.7 of the span of the measure scale) for each objective result in different performance status for each public policy with respect to the three objectives. Policy *p01*, for instance, will probably show *good* performances wrt the *economical* and environmental aspects, and *weak* performances wrt the *societal* aspect.

For testing purposes we provide a special `PartialPerformanceTableau`

class for extracting a **partial performance tableau** from a given tableau instance. In the example blow, we may construct the partial performance tableaux corresponding to each one of the three decision objectives.

```
1>>> from perfTabs import PartialPerformanceTableau
2>>> teco = PartialPerformanceTableau(t,criteriaSubset=\
3... t.objectives['Eco']['criteria'])
4
5>>> tsoc = PartialPerformanceTableau(t,criteriaSubset=\
6... t.objectives['Soc']['criteria'])
7
8>>> tenv = PartialPerformanceTableau(t,criteriaSubset=\
9... t.objectives['Env']['criteria'])
```

One may thus compute a partial bipolar-valued outranking digraph for each individual objective.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> geco = BipolarOutrankingDigraph(teco)
3>>> gsoc = BipolarOutrankingDigraph(tsoc)
4>>> genv = BipolarOutrankingDigraph(tenv)
```

The three partial digraphs: *geco*, *gsoc* and *genv*, hence model the preferences represented in each one of the partial performance tableaux. And, we may aggregate these three outranking digraphs with an epistemic fusion operator.

```
1>>> from digraphs import FusionLDigraph
2>>> gfus = FusionLDigraph([geco,gsoc,genv])
3>>> gfus.strongComponents()
4 {frozenset({'p30'}),
5 frozenset({'p10', 'p03', 'p19', 'p08', 'p07', 'p04', 'p21', 'p20',
6 'p13', 'p23', 'p16', 'p12', 'p24', 'p02', 'p31', 'p29',
7 'p05', 'p09', 'p28', 'p25', 'p17', 'p14', 'p15', 'p06',
8 'p01', 'p27', 'p11', 'p18', 'p22'}),
9 frozenset({'p26'})}
10>>> from digraphs import StrongComponentsCollapsedDigraph
11>>> scc = StrongComponentsCollapsedDigraph(gfus)
12>>> scc.showActions()
13 *----- show digraphs actions --------------*
14 key: frozenset({'p30'})
15 short name: Scc_1
16 name: _p30_
17 comment: collapsed strong component
18 key: frozenset({'p10', 'p03', 'p19', 'p08', 'p07', 'p04', 'p21', 'p20', 'p13',
19 'p23', 'p16', 'p12', 'p24', 'p02', 'p31', 'p29', 'p05', 'p09', 'p28', 'p25',
20 'p17', 'p14', 'p15', 'p06', 'p01', 'p27', 'p11', 'p18', 'p22'})
21 short name: Scc_2
22 name: _p10_p03_p19_p08_p07_p04_p21_p20_p13_p23_p16_p12_p24_p02_p31_\
23 p29_p05_p09_p28_p25_p17_p14_p15_p06_p01_p27_p11_p18_p22_
24 comment: collapsed strong component
25 key: frozenset({'p26'})
26 short name: Scc_3
27 name: _p26_
28 comment: collapsed strong component
```

A graphviz drawing illustrates the apparent preferential links between the strong components.

```
1>>> scc.exportGraphViz('scFusionObjectives')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to scFusionObjectives.dot
4 dot -Grankdir=BT -Tpng scFusionObjectives.dot -o scFusionObjectives.png
```

Public policy *p26* (Eco+ Soc+ Env-) appears dominating the other policies, whereas policy *p30* (Eco- Soc- Env-) appears to be dominated by all the others.

### 1.5.5. Random academic performance tableaux

The `RandomAcademicPerformanceTableau`

class generates temporary performance tableaux with random grades for a given number of students in different courses (see Lecture 4: *Grading*, Algorithmic decision Theory Course http://hdl.handle.net/10993/37933)

*Parameters*:

number of students,

number of courses,

weightDistribution := ‘equisignificant’ | ‘random’ (default)

weightScale := (1, 1 | numberOfCourses (default when random))

IntegerWeights := Boolean (True = default)

commonScale := (0,20) (default)

ndigits := 0

WithTypes := Boolean (False = default)

commonMode := (‘triangular’,xm=14,r=0.25) (default)

commonThresholds := {‘ind’:(0,0), ‘pref’:(1,0)} (default)

missingDataProbability := 0.0 (default)

NA := <Decimal> (default = -999); missing data symbol.

When parameter *WithTypes* is set to *True*, the students are randomly allocated to one of the four categories: *weak* (1/6), *fair* (1/3), *good* (1/3), and *excellent* (1/3), in the bracketed proportions. In a default 0-20 grading range, the random range of a weak student is 0-10, of a fair student 4-16, of a good student 8-20, and of an excellent student 12-20. The random grading generator follows in this case a double triangular probablity law with *mode* (*xm*) equal to the middle of the random range and *median repartition* (*r* = 0.5) of probability each side of the mode.

```
1>>> from randomPerfTabs import RandomAcademicPerformanceTableau
2>>> t = RandomAcademicPerformanceTableau(\
3... numberOfStudents=11,\
4... numberOfCourses=7, missingDataProbability=0.03,\
5... WithTypes=True, seed=100)
6
7>>> t
8 *------- PerformanceTableau instance description ------*
9 Instance class : RandomAcademicPerformanceTableau
10 Seed : 100
11 Instance name : randstudPerf
12 # Actions : 11
13 # Criteria : 7
14 Attributes : ['randomSeed', 'name', 'actions',
15 'criteria', 'evaluation', 'weightPreorder']
16>>> t.showPerformanceTableau()
17 *---- performance tableau -----*
18 Courses | 'm1' 'm2' 'm3' 'm4' 'm5' 'm6' 'm7'
19 ECTS | 2 1 3 4 1 1 5
20 ---------|------------------------------------------
21 's01f' | 12 13 15 08 16 06 15
22 's02g' | 10 15 20 11 14 15 18
23 's03g' | 14 12 19 11 15 13 11
24 's04f' | 13 15 12 13 13 10 06
25 's05e' | 12 14 13 16 15 12 16
26 's06g' | 17 13 10 14 NA 15 13
27 's07e' | 12 12 12 18 NA 13 17
28 's08f' | 14 12 09 13 13 15 12
29 's09g' | 19 14 15 13 09 13 16
30 's10g' | 10 12 14 17 12 16 09
31 's11w' | 10 10 NA 10 10 NA 08
32>>> t.weightPreorder
33 [['m2', 'm5', 'm6'], ['m1'], ['m3'], ['m4'], ['m7']]
```

The example tableau, generated for instance above with *missingDataProbability* = 0.03, *WithTypes* = True and *seed* = 100 (see Listing 1.15 Lines 2-5), results in a set of two excellent (*s05*, *s07*), five good (*s02*, *s03*, *s06*, *s09*, *s10*), three fair (*s01*, *s04*, *s08*) and one weak (*s11*) student performances. Notice that six students get a grade below the course validating threshold 10 and we observe four missing grades (NA), two in course *m5* and one in course *m3* and course *m6* (see Lines 21-31).

We may show a statistical summary of the students’ grades obtained in the heighest weighted course, namely *m7*, followed by a performance heatmap browser view showing a global ranking of the students’ performances from best to weakest.

```
1>>> t.showCourseStatistics('m7')
2 *----- Summary performance statistics ------*
3 Course name : g7
4 Course weight : 5
5 # Students : 11
6 grading scale : 0.00 - 20.00
7 # missing evaluations : 0
8 mean evaluation : 12.82
9 standard deviation : 3.79
10 maximal evaluation : 18.00
11 quantile Q3 (x_75) : 16.25
12 median evaluation : 14.00
13 quantile Q1 (x_25) : 10.50
14 minimal evaluation : 6.00
15 mean absolute difference : 4.30
16 standard difference deviation : 5.35
17>>> t.showHTMLPerformanceHeatmap(colorLevels=5,\
18... pageTitle='Ranking the students')
```

The ranking shown here in Fig. 1.14 is produced with the default NetFlows ranking rule. With a mean marginal correlation of +0.361 (see Listing 1.17 Lines 17-) associated with a low standard deviation (0.248), the result represents a rather *fair weighted consensus* made between the individual courses’ marginal rankings.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> g = BipolarOutrankingDigraph(t)
3>>> t.showRankingConsensusQuality(g.computeNetFlowsRanking())
4 Consensus quality of ranking:
5 ['s07', 's02', 's09', 's05', 's06', 's03', 's10',
6 's01', 's08', 's04', 's11']
7 criterion (weight): correlation
8 -------------------------------
9 m7 (0.294): +0.727
10 m4 (0.235): +0.309
11 m2 (0.059): +0.291
12 m3 (0.176): +0.200
13 m1 (0.118): +0.109
14 m6 (0.059): +0.091
15 m5 (0.059): +0.073
16 Summary:
17 Weighted mean marginal correlation (a): +0.361
18 Standard deviation (b) : +0.248
19 Ranking fairness (a)-(b) : +0.113
```

### 1.5.6. Random linearly ranked performance tableaux

Finally, we provide the `RandomRankPerformanceTableau`

class for generating multiple criteria ranked performance tableaux, i.e. on each criterion, all decision action’s evaluations appear linearly ordered without ties.

This type of random performance tableau is matching the `RandomLinearVotingProfile`

class provided by the `votingProfiles`

module.

*Parameters*:number of actions,

number of performance criteria,

weightDistribution := ‘equisignificant’ | ‘random’ (default, see above,)

weightScale := (1, 1 | numberOfCriteria (default when random)).

integerWeights := Boolean (True = default)

commonThresholds (default) := {

‘ind’:(0,0),‘pref’:(1,0),‘veto’:(numberOfActions,0)} (default)

Back to Content Table

## 1.6. How to create a new performance tableau instance

In this tutorial we illustrate a way of creating a new `PerformanceTableau`

instance by editing a template with 5 decision alternatives, 3 decision objectives and 6 performance criteria.

### 1.6.1. Editing a template file

For this purpose we provide the following perfTab_Template.py file in the *examples* directory of the **Digraph3** resources.

```
1#########################################################
2# Digraph3 documentation
3# Template for creating a new PerformanceTableau instance
4# (C) R. Bisdorff Mar 2021
5# Digraph3/examples/perfTab_Template.py
6##########################################################
7from decimal import Decimal
8from collections import OrderedDict
9#####
10# edit the decision actions
11# avoid special characters, like '_', '/' or ':',
12# in action identifiers and short names
13actions = OrderedDict([
14 ('a1', {
15 'shortName': 'action1',
16 'name': 'decision alternative a1',
17 'comment': 'some specific features of this alternative',
18 }),
19 ...
20 ...
21])
22#####
23# edit the decision objectives
24# adjust the list of performance criteria
25# and the total weight (sum of the criteria weights)
26# per objective
27objectives = OrderedDict([
28 ('obj1', {
29 'name': 'decision objective obj1',
30 'comment': "some specific features of this objective",
31 'criteria': ['g1', 'g2'],
32 'weight': Decimal('6'),
33 }),
34 ...
35 ...
36 ])
37#####
38# edit the performance criteria
39# adjust the objective reference
40# Left Decimal of a threshold = constant part and
41# right Decimal = proportional part of the threshold
42criteria = OrderedDict([
43 ('g1', {
44 'shortName': 'crit1',
45 'name': "performance criteria 1",
46 'objective': 'obj1',
47 'preferenceDirection': 'max',
48 'comment': 'measurement scale type and unit',
49 'scale': (Decimal('0.0'), Decimal('100.0'),
50 'thresholds': {'ind': (Decimal('2.50'), Decimal('0.0')),
51 'pref': (Decimal('5.00'), Decimal('0.0')),
52 'veto': (Decimal('60.00'), Decimal('0.0'))
53 },
54 'weight': Decimal('3'),
55 }),
56 ...
57 ...
58 ])
59#####
60# default missing data symbol = -999
61NA = Decimal('-999')
62#####
63# edit the performance evaluations
64# criteria to be minimized take negative grades
65evaluation = {
66 'g1': {
67 'a1':Decimal("41.0"),
68 'a2':Decimal("100.0"),
69 'a3':Decimal("63.0"),
70 'a4':Decimal('23.0'),
71 'a5': NA,
72 },
73 # g2 is of ordinal type and scale 0-10
74 'g2': {
75 'a1':Decimal("4"),
76 'a2':Decimal("10"),
77 'a3':Decimal("6"),
78 'a4':Decimal('2'),
79 'a5':Decimal('9'),
80 },
81 # g3 has preferenceDirection = 'min'
82 'g3': {
83 'a1':Decimal("-52.2"),
84 'a2':NA,
85 'a3':Decimal("-47.3"),
86 'a4':Decimal('-35.7'),
87 'a5':Decimal('-68.00'),
88 },
89 ...
90 ...
91 }
92####################
```

The template file, shown in Listing 1.18, contains first the instructions to import the required *Decimal* and *OrderedDict* classes (see Lines 7-8). Four main sections are following: the potential decision **actions**, the decision **objectives**, the performance **criteria**, and finally the performance **evaluation**.

### 1.6.2. Editing the decision alternatives

Decision alternatives are stored in attribute **actions** under the *OrderedDict* format (see the OrderedDict description in the Python documentation).

Required attributes of each decision alternative, besides the object **identifier**, are: **shortName**, **name** and **comment** (see Lines 15-17). The *shortName* attribute is essentially used when showing the performance tableau or the performance heatmap in a browser view.

Note

Mind that graphviz drawings require digraph actions’ (nodes) identifier strings without any special characters like _ or /.

Decision actions descriptions are stored in the order of which they appear in the stored instance file. The OrderedDict object keeps this given order when iterating over the decision alternatives.

The random performance tableau models presented in the previous tutorial use the *actions* attribute for storing special features of the decision alternatives. The *Cost-Benefit* model, for instance, uses a **type** attribute for distinguishing between *advantageous*, *neutral* and *cheap* alternatives. The *3-Objectives* model keeps a detailed record of the performance profile per decision objective and the corresponding random generators per performance criteria (see Lines 7- below).

```
1>>> t = Random3ObjectivesPerformanceTableau()
2>>> t.actions
3 OrderedDict([
4 ('p01', {'shortName': 'p01',
5 'name': 'action p01 Eco~ Soc- Env+',
6 'comment': 'random public policy',
7 'Eco': 'fair',
8 'Soc': 'weak',
9 'Env': 'good',
10 'profile': {'Eco':'fair',
11 'Soc':'weak',
12 'Env':'good'}
13 'generators': {'ec01': ('triangular', 50.0, 0.5),
14 'so02': ('triangular', 30.0, 0.5),
15 'en03': ('triangular', 70.0, 0.5),
16 ...
17 },
18 }
19 ),
20 ...
21 ])
```

The second section of the template file concerns the decision *objectives*.

### 1.6.3. Editing the decision objectives

The minimal required attributes (see Listing 1.18 Lines 27-33) of the ordered decision **objectives** dictionary, besides the individual objective identifiers, are **name**, **comment**, **criteria** (the list of significant performance criteria) and **weight** (the importance of the decision objective). The latter attribute contains the sum of the *significance* weights of the objective’s criteria list.

The **objectives** attribute is methodologically useful for specifying the performance criteria significance in building decision recommendations. Mostly, we assume indeed that decision objectives are all equally important and the performance criteria are equi-significant per objective. This is, for instance, the default setting in the random *3-Objectives* performance tableau model.

```
1>>> t = Random3ObjectivesPerformanceTableau()
2>>> t.objectives
3 OrderedDict([
4 ('Eco',
5 {'name': 'Economical aspect',
6 'comment': 'Random3ObjectivesPerformanceTableau generated',
7 'criteria': ['ec01', 'ec06', 'ec09'],
8 'weight': Decimal('48')}),
9 ('Soc',
10 {'name': 'Societal aspect',
11 'comment': 'Random3ObjectivesPerformanceTableau generated',
12 'criteria': ['so02', 'so12'],
13 'weight': Decimal('48')}),
14 ('Env',
15 {'name': 'Environmental aspect',
16 'comment': 'Random3ObjectivesPerformanceTableau generated',
17 'criteria': ['en03', 'en04', 'en05', 'en07',
18 'en08', 'en10', 'en11', 'en13'],
19 'weight': Decimal('48')})
20 ])
```

The importance weight sums up to 48 for each one of the three example decision objectives shown in Listing 1.19 (Lines 8,13 and 19), so that the significance of each one of the 3 economic criteria is set to 16, of both societal criteria is set to 24, and of each one of the 8 environmental criteria is set to 8.

Note

Mind that the **objectives** attribute is always present in a *PerformanceTableau* object instance, even when empty. In this case, we consider that each performance criterion canonically represents in fact its own decision objective. The criterion significance equals in this case the corresponding decision objective’s importance weight.

The third section of the template file concerns now the *performance criteria*.

### 1.6.4. Editing the family of performance criteria

In order to assess how well each potential decision alternative is satisfying a given decision objective, we need *performance criteria*, i.e. decimal-valued grading functions gathered in an ordered **criteria** dictionary. The required attributes (see Listing 1.20), besides the criteria identifiers, are the usual **shortName**, **name** and **comment**. Specific for a criterion are furthermore the **objective** reference, the significance **weight**, the grading **scale** (minimum and maximum performance values), the **preferenceDirection** (‘max’ or ‘min’) and the performance discrimination **thresholds**.

```
1criteria = OrderedDict([
2 ('g1', {
3 'shortName': 'crit1',
4 'name': "performance criteria 1",
5 'comment': 'measurement scale type and unit',
6 'objective': 'obj1',
7 'weight': Decimal('3'),
8 'scale': (Decimal('0.0'), Decimal('100.0'),
9 'preferenceDirection': 'max',
10 'thresholds': {'ind': (Decimal('2.50'), Decimal('0.0')),
11 'pref': (Decimal('5.00'), Decimal('0.0')),
12 'veto': (Decimal('60.00'), Decimal('0.0'))
13 },
14 }),
15 ...
16 ...])
```

In our bipolar-valued outranking approach, all performance criteria implement *decimal-valued* grading functions, where preferences are either *increasing* or *decreasing* with measured performances.

Note

In order to model a **coherent** performance tableau, the decision criteria must satisfy two methodological requirements:

Independance: Each decision criterion implements a grading that isfunctionally independentof the grading of the other decision criteria, i.e. the performance measured on one of the criteria does notconstrainthe performance measured on any other criterion.

Non redundancy: Each performance criterion is onlysignificantfor asingledecision objective.

In order to take into account any, usually *unavoidable*, **imprecision** of the performance grading procedures, we may specify three performance **discrimination thresholds**: an **indifference** (‘ind’), a **preference** (‘pref’) and a **considerable performance difference** (‘veto’) threshold (see Listing 1.20 Lines 10-12). The left decimal number of a threshold description tuple indicates a *constant part*, whereas the right decimal number indicates a proportional part.

On the template performance criterion *g1*, shown in Listing 1.20, we observe for instance a grading scale from 0.0 to 100.0 with a constant *indifference* threshold of 2.5, a constant *preference* threshold of 5.0, and a constant *considerable performance difference* threshold of 60.0. The latter theshold will trigger, the case given, a *polarisation* of the outranking statement [BIS-2013] .

In a random *Cost-Benefit* performance tableau model we may obtain by default the following content.

```
1>>> tcb = RandomCBPerformanceTableau()
2>>> tcb.showObjectives()
3 *------ decision objectives -------"
4 C: Costs
5 c1 random cardinal cost criterion 6
6 Total weight: 6.00 (1 criteria)
7 ...
8 ...
9>>> tcb.criteria
10 OrderedDict([
11 ('c1', {'preferenceDirection': 'min',
12 'scaleType': 'cardinal',
13 'objective': 'C',
14 'shortName': 'c1',
15 'name': 'random cardinal cost criterion',
16 'scale': (0.0, 100.0),
17 'weight': Decimal('6'),
18 'randomMode': ['triangular', 50.0, 0.5],
19 'comment': 'Evaluation generator: triangular law ...',
20 'thresholds':
21 OrderedDict([
22 ('ind', (Decimal('1.49'), Decimal('0'))),
23 ('pref', (Decimal('3.7'), Decimal('0'))),
24 ('veto', (Decimal('67.71'), Decimal('0')))
25 ])
26 }
27 ...
28 ...
29 ])
```

Criterion *c1* appears here (see Listing 1.21) to be a cardinal criterion to be minimized and significant for the *Costs* (*C*) decision objective. We may use the `showCriteria()`

method for printing the corresponding performance discrimination thresholds.

```
1>>> tcb.showCriteria(IntegerWeights=True)
2 *---- criteria -----*
3 c1 'Costs/random cardinal cost criterion'
4 Scale = (0.0, 100.0)
5 Weight = 6
6 Threshold ind : 1.49 + 0.00x ; percentile: 5.13
7 Threshold pref : 3.70 + 0.00x ; percentile: 10.26
8 Threshold veto : 67.71 + 0.00x ; percentile: 96.15
```

The *indifference* threshold on this criterion amounts to a constant value of 1.49 (Line 6 above). More or less 5% of the observed performance differences on this criterion appear hence to be **insignificant**. Similarly, with a preference threshold of 3.70, about 90% of the observed performance differences are preferentially **significant** (Line 7). Furthermore, 100.0 - 96.15 = 3.85% of the observed performance differences appear to be **considerable** (Line 8) and will trigger a *polarisation* of the corresponding outranking statements.

After the performance criteria description, we are ready for recording the actual *performance table*.

### 1.6.5. Editing the performance table

The individual grades of each decision alternative on each decision criterion are recorded in a double *criterion* x *action* dictionary called **evaluation** (see Listing 1.22). As we may encounter missing data cases, we previously define a *missing data* symbol **NA** which is set here to a value disjoint from all the measurement scales, by default *Decimal(‘-999’)* (Line 2).

```
1#----------
2NA = Decimal('-999')
3#----------
4evaluation = {
5 'g1': {
6 'a1':Decimal("41.0"),
7 'a2':Decimal("100.0"),
8 'a3':Decimal("63.0"),
9 'a4':Decimal('23.0'),
10 'a5': NA, # missing data
11 },
12 ...
13 ...
14 # g3 has preferenceDirection = 'min'
15 'g3': {
16 'a1':Decimal("-52.2"), # negative grades
17 'a2':NA,
18 'a3':Decimal("-47.3"),
19 'a4':Decimal('-35.7'),
20 'a5':Decimal('-68.00'),
21 },
22 ...
23 ...
24 }
```

Notice in Listing 1.22 (Lines 16- ) that on a criterion with *preferenceDirection* = ‘**min**’ all performance grades are recorded as **negative** values.

We may now inspect the eventually recorded complete template performance table.

```
1>>> from perfTabs import PerformanceTableau
2>>> t = PerformanceTableau('perfTab_Template')
3>>> t.showPerformanceTableau(ndigits=1)
4 *---- performance tableau -----*
5 Criteria | 'g1' 'g2' 'g3' 'g4' 'g5' 'g6'
6 Actions | 3 3 6 2 2 2
7 ---------|-----------------------------------------
8 'action1' | 41.0 4.0 -52.2 71.0 63.0 22.5
9 'action2' | 100.0 10.0 NA 89.0 30.7 75.0
10 'action3' | 63.0 6.0 -47.3 55.4 63.5 NA
11 'action4' | 23.0 2.0 -35.7 83.5 37.5 54.9
12 'action5' | NA 9.0 -68.0 10.0 88.0 75.0
```

We may furthermore compute the associated outranking digraph and check if we observe any polarised outranking situtations.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> g = BipolarOutrankingDigraph(t)
3>>> g.showVetos()
4 *---- Veto situations ---
5 number of veto situations : 1
6 1: r(a4 >= a2) = -0.44
7 criterion: g1
8 Considerable performance difference : -77.00
9 Veto discrimination threshold : -60.00
10 Polarisation: r(a4 >= a2) = -0.44 ==> -1.00
11 *---- Counter-veto situations ---
12 number of counter-veto situations : 1
13 1: r(a2 >= a4) = 0.56
14 criterion: g1
15 Considerable performance difference : 77.00
16 Counter-veto threshold : 60.00
17 Polarisation: r(a2 >= a4) = 0.56 ==> +1.00
```

Indeed, due to the considerable performance difference (77.00) oberved on performance criterion *g1*, alternative *a2* **for sure** *outranks* alternative *a4*, respectively *a4* **for sure** *does not outrank* *a2*.

### 1.6.6. Inspecting the template outranking relation

Let us have a look at the outranking relation table.

```
1>>> g.showRelationTable()
2 * ---- Relation Table -----
3 r | 'a1' 'a2' 'a3' 'a4' 'a5'
4 -----|-----------------------------------
5 'a1' | +1.00 -0.44 -0.22 -0.11 +0.06
6 'a2' | +0.44 +1.00 +0.33 +1.00 +0.28
7 'a3' | +0.67 -0.33 +1.00 +0.00 +0.17
8 'a4' | +0.11 -1.00 +0.00 +1.00 +0.06
9 'a5' | -0.06 -0.06 -0.17 -0.06 +1.00
```

We may notice in the outranking relation table above (see Listing 1.23) that decision alternative *a2* positively **outranks** all the other four alternatives (Line 6). Similarly, alternative *a5* is positively **outranked** by all the other alternatives (see Line 9). We may orient this way the *graphviz* drawing of the template outranking digraph.

```
>>> g.exportGraphViz(fileName= 'template',\
... firstChoice =['a2'],\
... lastChoice=['a5'])
*---- exporting a dot file for GraphViz tools ---------*
Exporting to template.dot
dot -Grankdir=BT -Tpng template.dot -o template.png
```

In Fig. 1.15 we may notice that the template outranking digraph models in fact a **partial order** on the five potential decision alternatives. Alternatives *action3* (‘a3’ ) and *action4* (‘a4’) appear actually **incomparable**. In Listing 1.23 their pairwise outranking chracteritics show indeed the **indeterminate** value 0.00 (Lines 7-8). We may check their pairwise comparison as follows.

```
1>>> g.showPairwiseComparison('a3','a4')
2 *------------ pairwise comparison ----*
3 Comparing actions : (a3, a4)
4 crit. wght. g(x) g(y) diff | ind pref r() |
5 ------------------------------- -------------------
6 g1 3.00 63.00 23.00 +40.00 | 2.50 5.00 +3.00 |
7 g2 3.00 6.00 2.00 +4.00 | 0.00 1.00 +3.00 |
8 g3 6.00 -47.30 -35.70 -11.60 | 0.00 10.00 -6.00 |
9 g4 2.00 55.40 83.50 -28.10 | 2.09 4.18 -2.00 |
10 g5 2.00 63.50 37.50 +26.00 | 0.00 10.00 +2.00 |
11 g6 NA 54.90
12 Outranking characteristic value: r(a3 >= a4) = +0.00
13 Valuation in range: -18.00 to +18.00
```

The incomparability situation between ‘a3’ and ‘a4’ results here from a perfect balancing of positive (+8) and negative (-8) criteria significance weights.

### 1.6.7. Ranking the template peformance tableau

We may eventually rank the five decision alternatives with a heatmap browser view following the *Copeland* ranking rule which consistently reproduces the partial outranking order shown in Fig. 1.15.

```
>>> g.showHTMLPerformanceHeatmap(ndigits=1,colorLevels=5,\
... Correlations=True,rankingRule='Copeland',\
... pageTitle='Heatmap of the template performance tableau')
```

Due to a 11 against 7 **plurality tyranny** effect, the *Copeland* ranking rule, essentially based on crisp majority outranking counts, puts here alternative *action5* (*a5*) last, despite its excellent grades observed on criteria *g2*, *g5* and *g6*. A slightly **fairer** ranking result may be obtained with the *NetFlows* ranking rule.

```
>>> g.showHTMLPerformanceHeatmap(ndigits=1,colorLevels=5,\
... Correlations=True,rankingRule='NetFlows',\
... pageTitle='Heatmap of the template performance tableau')
```

It might be opportun to furthermore study the robustness of the apparent outranking situations when assuming only *ordinal* or *uncertain* criteria significance weights. If interested in mainly objectively *unopposed* (multipartisan) outranking situations, one might also try the `UnOpposedOutrankingDigraph`

constructor. (see the advanced topics of the Digraph3 documentation).

Back to Content Table

## 1.7. Computing the winner of an election with the `votingProfiles`

module

### 1.7.1. Linear voting profiles

The `votingProfiles`

module provides resources for handling election results [ADT-L2], like the `LinearVotingProfile`

class. We consider an election involving a finite set of candidates and finite set of weighted voters, who express their voting preferences in a complete linear ranking (without ties) of the candidates. The data is internally stored in two ordered dictionaries, one for the voters and another one for the candidates. The linear ballots are stored in a standard dictionary.

```
1 candidates = OrderedDict([('a1',...), ('a2',...), ('a3', ...), ...}
2 voters = OrderedDict([('v1',{'weight':10}), ('v2',{'weight':3}), ...}
3 ## each voter specifies a linearly ranked list of candidates
4 ## from the best to the worst (without ties
5 linearBallot = {
6 'v1' : ['a2','a3','a1', ...],
7 'v2' : ['a1','a2','a3', ...],
8 ...
9 }
```

The module provides a `RandomLinearVotingProfile`

class for generating random instances of the `LinearVotingProfile`

class. In an interactive Python session we may obtain for the election of 3 candidates by 5 voters the following result.

```
1>>> from votingProfiles import RandomLinearVotingProfile
2>>> v = RandomLinearVotingProfile(numberOfVoters=5,\
3... numberOfCandidates=3,\
4... RandomWeights=True)
5
6>>> v.candidates
7 OrderedDict([ ('a1',{'name':'a1}), ('a2',{'name':'a2'}),
8 ('a3',{'name':'a3'}) ])
9>>> v.voters
10 OrderedDict([('v1',{'weight': 2}), ('v2':{'weight': 3}),
11 ('v3',{'weight': 1}), ('v4':{'weight': 5}),
12 ('v5',{'weight': 4})])
13>>> v.linearBallot
14 {'v1': ['a1', 'a2', 'a3',],
15 'v2': ['a3', 'a2', 'a1',],
16 'v3': ['a1', 'a3', 'a2',],
17 'v4': ['a1', 'a3', 'a2',],
18 'v5': ['a2', 'a3', 'a1',]}
```

Notice that in this random example, the five voters are weighted (see Listing 1.24 Lines 10-12). Their linear ballots can be viewed with the `showLinearBallots()`

method.

```
1>>> v.showLinearBallots()
2 voters(weight) candidates rankings
3 v1(2): ['a2', 'a1', 'a3']
4 v2(3): ['a3', 'a1', 'a2']
5 v3(1): ['a1', 'a3', 'a2']
6 v4(5): ['a1', 'a2', 'a3']
7 v5(4): ['a3', 'a1', 'a2']
8 # voters: 15
```

Editing of the linear voting profile may be achieved by storing the data in a file, edit it, and reload it again.

```
1>>> v.save(fileName='tutorialLinearVotingProfile1')
2 *--- Saving linear profile in file: <tutorialLinearVotingProfile1.py> ---*
3>>> from votingProfiles import LinearVotingProfile
4>>> v = LinearVotingProfile('tutorialLinearVotingProfile1')
```

### 1.7.2. Computing the winner

We may easily compute **uni-nominal votes**, i.e. how many times a candidate was ranked first, and see who is consequently the **simple majority** winner(s) in this election.

```
1>>> v.computeUninominalVotes()
2 {'a2': 2, 'a1': 6, 'a3': 7}
3>>> v.computeSimpleMajorityWinner()
4 ['a3']
```

As we observe no absolute majority (8/15) of votes for any of the three candidate, we may look for the **instant runoff** winner instead (see [ADT-L2]).

```
1>>> v.computeInstantRunoffWinner(Comments=True)
2 Half of the Votes = 7.50
3 ==> stage = 1
4 remaining candidates ['a1', 'a2', 'a3']
5 uninominal votes {'a1': 6, 'a2': 2, 'a3': 7}
6 minimal number of votes = 2
7 maximal number of votes = 7
8 candidate to remove = a2
9 remaining candidates = ['a1', 'a3']
10 ==> stage = 2
11 remaining candidates ['a1', 'a3']
12 uninominal votes {'a1': 8, 'a3': 7}
13 minimal number of votes = 7
14 maximal number of votes = 8
15 candidate a1 obtains an absolute majority
16 Instant run off winner: ['a1']
```

In stage 1, no candidate obtains an absolute majority of votes. Candidate *a2* obtains the minimal number of votes (2/15) and is, hence, eliminated. In stage 2, candidate *a1* obtains an absolute majority of the votes (8/15) and is eventually elected (see Listing 1.25).

We may also follow the *Chevalier de Borda*’s advice and, after a **rank analysis** of the linear ballots, compute the **Borda score** -the average rank- of each candidate and hence determine the *Borda* **winner(s)**.

```
1>>> v.computeRankAnalysis()
2 {'a2': [2, 5, 8], 'a1': [6, 9, 0], 'a3': [7, 1, 7]}
3>>> v.computeBordaScores()
4 OrderedDict([
5 ('a1', {'BordaScore': 24, 'averageBordaScore': 1.6}),
6 ('a3', {'BordaScore': 30, 'averageBordaScore': 2.0}),
7 ('a2', {'BordaScore': 36, 'averageBordaScore': 2.4}) ])
8>>> v.computeBordaWinners()
9 ['a1']
```

Candidate *a1* obtains the minimal *Borda* score, followed by candidate *a3* and finally candidate *a2* (see Listing 1.26). The corresponding *Borda* **rank analysis table** may be printed out with a corresponding `show()`

command.

```
1>>> v.showRankAnalysisTable()
2 *---- Borda rank analysis tableau -----*
3 candi- | alternative-to-rank | Borda
4 dates | 1 2 3 | score average
5 -------|-------------------------------------
6 'a1' | 6 9 0 | 24/15 1.60
7 'a3' | 7 1 7 | 30/15 2.00
8 'a2' | 2 5 8 | 36/15 2.40
```

In our randomly generated election results, we are lucky: The instant runoff winner and the *Borda* winner both are candidate *a1* (see Listing 1.25 and Listing 1.27). However, we could also follow the *Marquis de Condorcet*’s advice, and compute the **majority margins** obtained by voting for each individual pair of candidates.

### 1.7.3. The Condorcet winner

For instance, candidate *a1* is ranked four times before and once behind candidate *a2*. Hence the corresponding **majority margin** *M(a1,a2)* is 4 - 1 = +3. These *majority margins* define on the set of candidates what we call the **majority margins digraph**. The `MajorityMarginsDigraph`

class (a specialization of the `Digraph`

class) is available for handling such kind of digraphs.

```
1>>> from votingProfiles import MajorityMarginsDigraph
2>>> cdg = MajorityMarginsDigraph(v,IntegerValuation=True)
3>>> cdg
4 *------- Digraph instance description ------*
5 Instance class : MajorityMarginsDigraph
6 Instance name : rel_randomLinearVotingProfile1
7 Digraph Order : 3
8 Digraph Size : 3
9 Valuation domain : [-15.00;15.00]
10 Determinateness (%) : 64.44
11 Attributes : ['name', 'actions', 'voters',
12 'ballot', 'valuationdomain',
13 'relation', 'order',
14 'gamma', 'notGamma']
15>>> cdg.showAll()
16 *----- show detail -------------*
17 Digraph : rel_randLinearVotingProfile1
18 *---- Actions ----*
19 ['a1', 'a2', 'a3']
20 *---- Characteristic valuation domain ----*
21 {'max': Decimal('15.0'), 'med': Decimal('0'),
22 'min': Decimal('-15.0'), 'hasIntegerValuation': True}
23 * ---- majority margins -----
24 M(x,y) | 'a1' 'a2' 'a3'
25 ----------|-------------------
26 'a1' | 0 11 1
27 'a2' | -11 0 -1
28 'a3' | -1 1 0
29 Valuation domain: [-15;+15]
```

Notice that in the case of linear voting profiles, majority margins always verify a zero sum property: *M(x,y)* + *M(y,x)* = 0 for all candidates *x* and *y* (see Listing 1.28 Lines 26-28). This is not true in general for arbitrary voting profiles. The *majority margins* digraph of linear voting profiles defines in fact a *weak tournament* and belongs, hence, to the class of *self-codual* bipolar-valued digraphs (13).

Now, a candidate *x*, showing a positive majority margin *M(x,y)*, is beating candidate *y* with an absolute majority in a pairwise voting. Hence, a candidate showing only positive terms in her row in the *majority margins* digraph relation table, beats all other candidates with absolute majority of votes. Condorcet recommends to declare this candidate (is always unique, why?) the winner of the election. Here we are lucky, it is again candidate *a1* who is hence the **Condorcet winner** (see Listing 1.28 Line 26).

```
1>>> cdg.computeCondorcetWinners()
2 ['a1']
```

By seeing the majority margins like a *bipolar-valued characteristic function* of a global preference relation defined on the set of candidates, we may use all operational resources of the generic `Digraph`

class (see Working with the Digraph3 software resources), and especially its `exportGraphViz()`

method 1, for visualizing an election result.

```
1>>> cdg.exportGraphViz(fileName='tutorialLinearBallots')
2*---- exporting a dot file for GraphViz tools ---------*
3Exporting to tutorialLinearBallots.dot
4dot -Grankdir=BT -Tpng tutorialLinearBallots.dot -o tutorialLinearBallots.png
```

In Fig. 1.16 we notice that the *majority margins* digraph from our example linear voting profile gives a linear order of the candidates: [‘a1’, ‘a3’, ‘a2], the same actually as given by the *Borda* scores (see Listing 1.26). This is by far not given in general. Usually, when aggregating linear ballots, there appear cyclic social preferences.

### 1.7.5. On generating realistic random linear voting profiles

By default, the `RandomLinearVotingProfile`

class generates random linear voting profiles where every candidates has the same uniform probabilities to be ranked at a certain position by all the voters. For each voter’s random linear ballot is indeed generated via a uniform shuffling of the list of candidates.

In reality, political election data appear quite different. There will usually be different favorite and marginal candidates for each political party. To simulate these aspects into our random generator, we are using two random exponentially distributed polls of the candidates and consider a bipartisan political landscape with a certain random balance (default theoretical party repartition = 0.50) between the two sets of potential party supporters (see `LinearVotingProfile`

class). A certain theoretical proportion (default = 0.1) will not support any party.

Let us generate such a linear voting profile for an election with 1000 voters and 15 candidates.

```
1>>> from votingProfiles import RandomLinearVotingProfile
2>>> lvp = RandomLinearVotingProfile(numberOfCandidates=15,\
3... numberOfVoters=1000,
4... WithPolls=True,
5... partyRepartition=0.5,
6... other=0.1,
7... seed=0.9189670954954139)
8
9>>> lvp
10 *------- VotingProfile instance description ------*
11 Instance class : RandomLinearVotingProfile
12 Instance name : randLinearProfile
13 # Candidates : 15
14 # Voters : 1000
15 Attributes : ['name', 'seed', 'candidates',
16 'voters', 'RandomWeights',
17 'sumWeights', 'poll1', 'poll2',
18 'bipartisan', 'linearBallot', 'ballot']
19>>> lvp.showRandomPolls()
20 Random repartition of voters
21 Party_1 supporters : 460 (46.0%)
22 Party_2 supporters : 436 (43.6%)
23 Other voters : 104 (10.4%)
24 *---------------- random polls ---------------
25 Party_1(46.0%) | Party_2(43.6%)| expected
26 -----------------------------------------------
27 a06 : 19.91% | a11 : 22.94% | a06 : 15.00%
28 a07 : 14.27% | a08 : 15.65% | a11 : 13.08%
29 a03 : 10.02% | a04 : 15.07% | a08 : 09.01%
30 a13 : 08.39% | a06 : 13.40% | a07 : 08.79%
31 a15 : 08.39% | a03 : 06.49% | a03 : 07.44%
32 a11 : 06.70% | a09 : 05.63% | a04 : 07.11%
33 a01 : 06.17% | a07 : 05.10% | a01 : 05.06%
34 a12 : 04.81% | a01 : 05.09% | a13 : 05.04%
35 a08 : 04.75% | a12 : 03.43% | a15 : 04.23%
36 a10 : 04.66% | a13 : 02.71% | a12 : 03.71%
37 a14 : 04.42% | a14 : 02.70% | a14 : 03.21%
38 a05 : 04.01% | a15 : 00.86% | a09 : 03.10%
39 a09 : 01.40% | a10 : 00.44% | a10 : 02.34%
40 a04 : 01.18% | a05 : 00.29% | a05 : 01.97%
41 a02 : 00.90% | a02 : 00.21% | a02 : 00.51%
```

In this example (see Listing 1.31 Lines 19-), we obtain 460 Party_1 supporters (46%), 436 Party_2 supporters (43.6%) and 104 other voters (10.4%). Favorite candidates of *Party_1* supporters, with more than 10%, appear to be *a06* (19.91%), *a07* (14.27%) and *a03* (10.02%). Whereas for *Party_2* supporters, favorite candidates appear to be *a11* (22.94%), followed by *a08* (15.65%), *a04* (15.07%) and *a06* (13.4%). Being *first* choice for *Party_1* supporters and *fourth* choice for *Party_2* supporters, this candidate *a06* is a natural candidate for clearly winning this election game (see Listing 1.32).

```
1>>> lvp.computeSimpleMajorityWinner()
2 ['a06']
3>>> lvp.computeInstantRunoffWinner()
4 ['a06']
5>>> lvp.computeBordaWinners()
6 ['a06']
```

Is it also a *Condorcet* winner ? To verify, we start by creating the corresponding *majority margins* digraph *cdg* with the help of the `MajorityMarginsDigraph`

class. The created digraph instance contains 15 *actions* -the candidates- and 105 *oriented* arcs -the *positive* majority margins- (see Listing 1.33 Lines 7-8).

```
1>>> from votingProfiles import MajorityMarginsDigraph
2>>> cdg = MajorityMarginsDigraph(lvp)
3>>> cdg
4 *------- Digraph instance description ------*
5 Instance class : MajorityMarginsDigraph
6 Instance name : rel_randLinearProfile
7 Digraph Order : 15
8 Digraph Size : 104
9 Valuation domain : [-1000.00;1000.00]
10 Determinateness (%) : 67.08
11 Attributes : ['name', 'actions', 'voters',
12 'ballot', 'valuationdomain',
13 'relation', 'order',
14 'gamma', 'notGamma']
```

We may visualize the resulting pairwise majority margins by showing the HTML formated version of the *cdg* relation table in a browser view.

```
>>> cdg.showHTMLRelationTable(tableTitle='Pairwise majority margins',
... relationName='M(x>y)')
```

In Fig. 1.19, *light green* cells contain the positive majority margins, whereas *light red* cells contain the negative majority margins. A complete *light green* row reveals hence a *Condorcet* **winner**, whereas a complete *light green* column reveals a *Condorcet* **loser**. We recover again candidate *a06* as *Condorcet* winner (15), whereas the obvious *Condorcet* loser is here candidate *a02*, the candidate with the lowest support in both parties (see Listing 1.31 Line 40).

With a same *bipolar* -*first ranked* and *last ranked* candidate- selection procedure, we may *weakly rank* the candidates (with possible ties) by iterating these *first ranked* and *last ranked* choices among the remaining candidates ([BIS-1999]).

```
1>>> cdg.showRankingByChoosing()
2 Error: You must first run
3 self.computeRankingByChoosing(CoDual=False(default)|True) !
4>>> cdg.computeRankingByChoosing()
5>>> cdg.showRankingByChoosing()
6 Ranking by Choosing and Rejecting
7 1st first ranked ['a06']
8 2nd first ranked ['a11']
9 3rd first ranked ['a07', 'a08']
10 4th first ranked ['a03']
11 5th first ranked ['a01']
12 6th first ranked ['a13']
13 7th first ranked ['a04']
14 7th last ranked ['a12']
15 6th last ranked ['a14']
16 5th last ranked ['a15']
17 4th last ranked ['a09']
18 3rd last ranked ['a10']
19 2nd last ranked ['a05']
20 1st last ranked ['a02']
```

Before showing the *ranking-by-choosing* result, we have to compute the iterated bipolar selection procedure (see Listing 1.34 Line 2). The first selection concerns *a06* (first) and *a02* (last), followed by *a11* (first) opposed to *a05* (last), and so on, until there remains at iteration step 7 a last pair of candidates, namely *[a04, a12]* (see Lines 13-14).

Notice furthermore the first ranked candidates at iteration step 3 (see Listing 1.34 Line 9), namely the pair *[a07, a08]*. Both candidates represent indeed conjointly the *first ranked* choice. We obtain here hence a *weak ranking*, i.e. a ranking with a tie.

Let us mention that the *instant-run-off* procedure, we used before (see Listing 1.32 Line 3), when operated with a *Comments=True* parameter setting, will deliver a more or less similar *reversed* linear *ordering-by-rejecting* result, namely [*a02*, *a10*, *a14*, *a05*, *a09*, *a13*, *a12*, *a15*, *a04*, *a01*, *a08*, *a03*, *a07*, *a11*, *a06*], ordered from the *last* to the *first* choice.

Remarkable about both these *ranking-by-choosing* or *ordering-by-rejecting* results is the fact that the random voting behaviour, simulated here with the help of two discrete random variables (16), defined respectively by the two party polls, is rendering a ranking that is more or less in accordance with the simulated balance of the polls: -*Party_1* supporters : 460; *Party_2* supporters: 436 (see Listing 1.31 Lines 26-40 third column). Despite a random voting behaviour per voter, the given polls apparently show a *very strong incidence* on the eventual election result. In order to avoid any manipulation of the election outcome, public media are therefore in some countries not allowed to publish polls during the last weeks before a general election.

Note

Mind that the specific *ranking-by-choosing* procedure, we use here on the *majority margins* digraph, operates the selection procedure by extracting at each step *initial* and *terminal* kernels, i.e. NP-hard operational problems (see tutorial on computing kernels and [BIS-1999]); A technique that does not allow in general to tackle voting profiles with much more than 30 candidates. The tutorial on ranking provides more adequate and efficient techniques for ranking from pairwise majority margins when a larger number of potential candidates is given.

Back to Content Table

## 1.8. Ranking with multiple incommensurable criteria

### 1.8.1. The ranking problem

We need to rank without ties a set *X* of items (usually decision alternatives) that are evaluated on multiple incommensurable performance criteria; yet, for which we may know their pairwise bipolar-valued *strict outranking* characteristics, i.e. for all *x*, *y* in *X* (see The strict outranking digraph and [BIS-2013]).

Let us consider a didactic outranking digraph *g* generated from a random Cost-Benefit performance tableau concerning 9 decision alternatives evaluated on 13 performance criteria. We may compute the corresponding *strict outranking digraph* with a codual transform as follows.

```
1>>> from outrankingDigraphs import *
2>>> t = RandomCBPerformanceTableau(numberOfActions=9,\
3... numberOfCriteria=13,seed=200)
4
5>>> g = BipolarOutrankingDigraph(t,Normalized=True)
6>>> gcd = ~(-g) # codual digraph
7>>> gcd.showRelationTable(ReflexiveTerms=False)
8 * ---- Relation Table -----
9 r(>) | 'a1' 'a2' 'a3' 'a4' 'a5' 'a6' 'a7' 'a8' 'a9'
10 -----|------------------------------------------------------
11 'a1' | - 0.00 +0.10 -1.00 -0.13 -0.57 -0.23 +0.10 +0.00
12 'a2' | -1.00 - 0.00 +0.00 -0.37 -0.42 -0.28 -0.32 -0.12
13 'a3' | -0.10 0.00 - -0.17 -0.35 -0.30 -0.17 -0.17 +0.00
14 'a4' | 0.00 0.00 -0.42 - -0.40 -0.20 -0.60 -0.27 -0.30
15 'a5' | +0.13 +0.22 +0.10 +0.40 - +0.03 +0.40 -0.03 -0.07
16 'a6' | -0.07 -0.22 +0.20 +0.20 -0.37 - +0.10 -0.03 -0.07
17 'a7' | -0.20 +0.28 -0.03 -0.07 -0.40 -0.10 - +0.27 +1.00
18 'a8' | -0.10 -0.02 -0.23 -0.13 -0.37 +0.03 -0.27 - +0.03
19 'a9' | 0.00 +0.12 -1.00 -0.13 -0.03 -0.03 -1.00 -0.03 -
```

Some ranking rules will work on the associated **Condorcet Digraph**, i.e. the corresponding *strict median cut* polarised digraph.

```
1>>> ccd = PolarisedOutrankingDigraph(gcd,\
2... level=g.valuationdomain['med'],\
3... KeepValues=False,StrictCut=True)
4
5>>> ccd.showRelationTable(ReflexiveTerms=False,IntegerValues=True)
6 *---- Relation Table -----
7 r(>)_med | 'a1' 'a2' 'a3' 'a4' 'a5' 'a6' 'a7' 'a8' 'a9'
8 ---------|---------------------------------------------
9 'a1' | - 0 +1 -1 -1 -1 -1 +1 0
10 'a2' | -1 - +0 0 -1 -1 -1 -1 -1
11 'a3' | -1 0 - -1 -1 -1 -1 -1 0
12 'a4' | 0 0 -1 - -1 -1 -1 -1 -1
13 'a5' | +1 +1 +1 +1 - +1 +1 -1 -1
14 'a6' | -1 -1 +1 +1 -1 - +1 -1 -1
15 'a7' | -1 +1 -1 -1 -1 -1 - +1 +1
16 'a8' | -1 -1 -1 -1 -1 +1 -1 - +1
17 'a9' | 0 +1 -1 -1 -1 -1 -1 -1 -
```

Unfortunately, such crisp median-cut *Condorcet* digraphs, associated with a given strict outranking digraph, present only exceptionally a linear ordering. Usually, pairwise majority comparisons do not even render a *complete* or, at least, a *transitive* partial order. There may even frequently appear *cyclic* outranking situations (see the tutorial on linear voting profiles).

To estimate how *difficult* this ranking problem here may be, we may have a look at the corresponding strict outranking digraph *graphviz* drawing (1).

```
1>>> gcd.exportGraphViz('rankingTutorial')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to rankingTutorial.dot
4 dot -Grankdir=BT -Tpng rankingTutorial.dot -o rankingTutorial.png
```

The strict outranking relation shown here is apparently *not transitive*: for instance, alternative *a8* outranks alternative *a6* and alternative *a6* outranks *a4*, however *a8* does not outrank *a4* (see Fig. 1.20). We may compute the transitivity degree of the outranking digraph, i.e. the ratio of the difference between the number of outranking arcs and the number of transitive arcs over the difference of the number of arcs of the transitive closure minus the transitive arcs of the digraph *gcd*.

```
>>> gcd.computeTransitivityDegree(Comments=True)
Transitivity degree of graph <codual_rel_randomCBperftab>
#triples x>y>z: 78, #closed: 38, #open: 40
#closed/#triples = 0.487
```

With only 35% of the required transitive arcs, the strict outranking relation here is hence very far from being transitive; a serious problem when a linear ordering of the decision alternatives is looked for. Let us furthermore see if there are any cyclic outrankings.

```
1>>> gcd.computeChordlessCircuits()
2>>> gcd.showChordlessCircuits()
3 1 circuit(s).
4 *---- Chordless circuits ----*
5 1: ['a6', 'a7', 'a8'] , credibility : 0.033
```

There is one chordless circuit detected in the given strict outranking digraph *gcd*, namely *a6* outranks *a7*, the latter outranks *a8*, and *a8* outranks again *a6* (see Fig. 1.20). Any potential linear ordering of these three alternatives will, in fact, always contradict somehow the given outranking relation.

Now, several heuristic ranking rules have been proposed for constructing a linear ordering which is closest in some specific sense to a given outranking relation.

The Digraph3 resources provide some of the most common of these ranking rules, like *Copeland*’s, *Kemeny*’s, *Slater*’s, *Kohler*’s, *Arrow-Raynaud*’s or *Tideman*’s ranking rule.

### 1.8.2. The *Copeland* ranking

*Copeland*’s rule, the most intuitive one as it works well for any strict outranking relation which models in fact a linear order, works on the *median cut* strict outranking digraph *ccd*. The rule computes for each alternative a score resulting from the sum of the differences between the crisp **strict outranking** characteristics and the crisp **strict outranked** characteristics for all pairs of alternatives where *y* is different from *x*. The alternatives are ranked in decreasing order of these *Copeland* scores; ties, the case given, being resolved by a lexicographical rule.

```
1>>> from linearOrders import CopelandRanking
2>>> cop = CopelandRanking(gcd,Comments=True)
3 Copeland decreasing scores
4 a5 : 12
5 a1 : 2
6 a6 : 2
7 a7 : 2
8 a8 : 0
9 a4 : -3
10 a9 : -3
11 a3 : -5
12 a2 : -7
13 Copeland Ranking:
14 ['a5', 'a1', 'a6', 'a7', 'a8', 'a4', 'a9', 'a3', 'a2']
```

Alternative *a5* obtains here the best *Copeland* score (+12), followed by alternatives *a1*, *a6* and *a7* with same score (+2); following the lexicographic rule, *a1* is hence ranked before *a6* and *a6* before *a7*. Same situation is observed for *a4* and *a9* with a score of -3 (see Listing 1.37 Lines 4-12).

*Copeland*’s ranking rule appears in fact **invariant** under the codual transform and renders a same linear order indifferently from digraphs *g* or *gcd* . The resulting ranking (see Listing 1.37 Line 14) is rather correlated (+0.463) with the given pairwise outranking relation in the ordinal *Kendall* sense (see Listing 1.38).

```
1>>> corr = g.computeRankingCorrelation(cop.copelandRanking)
2>>> g.showCorrelation(corr)
3 Correlation indexes:
4 Crisp ordinal correlation : +0.463
5 Valued equivalalence : +0.107
6 Epistemic determination : 0.230
```

With an epistemic determination level of 0.230, the *extended Kendall tau* index (see [BIS-2012]) is in fact computed on 61.5% (100.0 x (1.0 + 0.23)/2) of the pairwise strict outranking comparisons. Furthermore, the bipolar-valued *relational equivalence* characteristics between the strict outranking relation and the *Copeland* ranking equals +0.107, i.e. a *majority* of 55.35% of the criteria significance supports the relational equivalence between the given strict outranking relation and the corresponding *Copeland* ranking.

The *Copeland* scores deliver actually only a unique *weak ranking*, i.e. a ranking with potential ties. This weak ranking may be constructed with the `WeakCopelandOrder`

class.

```
1>>> from transitiveDigraphs import WeakCopelandOrder
2>>> wcop = WeakCopelandOrder(g)
3>>> wcop.showRankingByChoosing()
4 Ranking by Choosing and Rejecting
5 1st ranked ['a5']
6 2nd ranked ['a1', 'a6', 'a7']
7 3rd ranked ['a8']
8 3rd last ranked ['a4', 'a9']
9 2nd last ranked ['a3']
10 1st last ranked ['a2']
```

We recover in Listing 1.39 above, the ranking with ties delivered by the *Copeland* scores (see Listing 1.37). We may draw its corresponding *Hasse* diagram (see Listing 1.40).

```
1>>> wcop.exportGraphViz(fileName='weakCopelandRanking')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to weakCopelandRanking.dot
4 0 { rank = same; a5; }
5 1 { rank = same; a1; a7; a6; }
6 2 { rank = same; a8; }
7 3 { rank = same; a4; a9}
8 4 { rank = same; a3; }
9 5 { rank = same; a2; }
10 dot -Grankdir=TB -Tpng weakCopelandRanking.dot\
11 -o weakCopelandRanking.png
```

Let us now consider a similar ranking rule, but working directly on the *bipolar-valued* outranking digraph.

### 1.8.3. The *NetFlows* ranking

The valued version of the *Copeland* rule, called **NetFlows** rule, computes for each alternative *x* a *net flow* score, i.e. the sum of the differences between the **strict outranking** characteristics and the **strict outranked** characteristics for all pairs of alternatives where *y* is different from *x*.

```
1:linenos:
2
3>>> from linearOrders import NetFlowsRanking
4>>> nf = NetFlowsRanking(gcd,Comments=True)
5 Net Flows :
6 a5 : 3.600
7 a7 : 2.800
8 a6 : 1.300
9 a3 : 0.033
10 a1 : -0.400
11 a8 : -0.567
12 a4 : -1.283
13 a9 : -2.600
14 a2 : -2.883
15 NetFlows Ranking:
16 ['a5', 'a7', 'a6', 'a3', 'a1', 'a8', 'a4', 'a9', 'a2']
17>>> cop.copelandRanking
18 ['a5', 'a1', 'a6', 'a7', 'a8', 'a4', 'a9', 'a3', 'a2']
```

It is worthwhile noticing again, that similar to the *Copeland* ranking rule seen before, the *NetFlows* ranking rule is also **invariant** under the codual transform and delivers again the same ranking result indifferently from digraphs *g* or *gcd* (see Listing 1.41 Line 14).

In our example here, the *NetFlows* scores deliver a ranking *without ties* which is rather different from the one delivered by *Copeland*’s rule (see Listing 1.41 Line 16). It may happen, however, that we obtain, as with the *Copeland* scores above, only a ranking with ties, which may then be resolved again by following a lexicographic rule. In such cases, it is possible to construct again a *weak ranking* with the corresponding `WeakNetFlowsOrder`

class.

The **NetFlows** ranking result appears to be slightly better correlated (+0.638) with the given outranking relation than its crisp cousin, the *Copeland* ranking (see Listing 1.38 Lines 4-6).

```
1>>> corr = gcd.computeOrdinalCorrelation(nf)
2>>> gcd.showCorrelation(corr)
3 Correlation indexes:
4 Extended Kendall tau : +0.638
5 Epistemic determination : 0.230
6 Bipolar-valued equivalence : +0.147
```

Indeed, the extended *Kendall* tau index of +0.638 leads to a bipolar-valued *relational equivalence* characteristics of +0.147, i.e. a *majority* of 57.35% of the criteria significance supports the relational equivalence between the given outranking digraphs *g* or *gcd* and the corresponding *NetFlows* ranking. This lesser ranking performance of the *Copeland* rule stems in this example essentially from the *weakness* of the actual ranking result and our subsequent *arbitrary* lexicographic resolution of the many ties given by the *Copeland* scores (see Fig. 1.21).

To appreciate now the more or less correlation of both the *Copeland* and the *NetFlows* rankings with the underlying pairwise outranking relation, it is useful to consider *Kemeny*’s and *Slater*’s **best fitting** ranking rules.

### 1.8.4. *Kemeny* rankings

A **Kemeny** ranking is a linear ranking without ties which is *closest*, in the sense of the ordinal *Kendall* distance (see [BIS-2012]), to the given valued outranking digraphs *g* or *gcd*. This rule is also *invariant* under the *codual* transform.

```
1>>> from linearOrders import KemenyRanking
2>>> ke = KemenyRanking(gcd,orderLimit=9) # default orderLimit is 7
3>>> ke.showRanking()
4 ['a5', 'a6', 'a7', 'a3', 'a9', 'a4', 'a1', 'a8', 'a2']
5>>> corr = gcd.computeOrdinalCorrelation(ke)
6>>> gcd.showCorrelation(corr)
7 Correlation indexes:
8 Extended Kendall tau : +0.779
9 Epistemic determination : 0.230
10 Bipolar-valued equivalence : +0.179
```

So, **+0.779** represents the *highest possible* ordinal correlation (fitness) any potential linear ranking can achieve with the given pairwise outranking digraph (see Listing 1.43 Lines 7-10).

A *Kemeny* ranking may not be unique. In our example here, we obtain in fact two *Kemeny* rankings with a same **maximal** *Kemeny* index of 12.9.

```
1>>> ke.maximalRankings
2 [['a5', 'a6', 'a7', 'a3', 'a8', 'a9', 'a4', 'a1', 'a2'],
3 ['a5', 'a6', 'a7', 'a3', 'a9', 'a4', 'a1', 'a8', 'a2']]
4>>> ke.maxKemenyIndex
5 Decimal('12.9166667')
```

We may visualize the partial order defined by the epistemic disjunction of both optimal *Kemeny* rankings by using the `RankingsFusion`

class as follows.

```
1>>> from transitiveDigraphs import RankingsFusion
2>>> wke = RankingsFusion(ke,ke.maximalRankings)
3>>> wke.exportGraphViz(fileName='tutorialKemeny')
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to tutorialKemeny.dot
6 0 { rank = same; a5; }
7 1 { rank = same; a6; }
8 2 { rank = same; a7; }
9 3 { rank = same; a3; }
10 4 { rank = same; a9; a8; }
11 5 { rank = same; a4; }
12 6 { rank = same; a1; }
13 7 { rank = same; a2; }
14 dot -Grankdir=TB -Tpng tutorialKemeny.dot -o tutorialKemeny.png
```

It is interesting to notice in Fig. 1.22 and Listing 1.44, that both *Kemeny* rankings only differ in their respective positioning of alternative *a8*; either before or after alternatives *a9*, *a4* and *a1*.

To choose now a specific representative among all the potential rankings with maximal Kemeny index, we will choose, with the help of the `showRankingConsensusQuality()`

method, the *most consensual* one.

```
1>>> g.showRankingConsensusQuality(ke.maximalRankings[0])
2 Consensus quality of ranking:
3 ['a5', 'a6', 'a7', 'a3', 'a8', 'a9', 'a4', 'a1', 'a2']
4 criterion (weight): correlation
5 -------------------------------
6 b09 (0.050): +0.361
7 b04 (0.050): +0.333
8 b08 (0.050): +0.292
9 b01 (0.050): +0.264
10 c01 (0.167): +0.250
11 b03 (0.050): +0.222
12 b07 (0.050): +0.194
13 b05 (0.050): +0.167
14 c02 (0.167): +0.000
15 b10 (0.050): +0.000
16 b02 (0.050): -0.042
17 b06 (0.050): -0.097
18 c03 (0.167): -0.167
19 Summary:
20 Weighted mean marginal correlation (a): +0.099
21 Standard deviation (b) : +0.177
22 Ranking fairness (a)-(b) : -0.079
23>>> g.showRankingConsensusQuality(ke.maximalRankings[1])
24 Consensus quality of ranking:
25 ['a5', 'a6', 'a7', 'a3', 'a9', 'a4', 'a1', 'a8', 'a2']
26 criterion (weight): correlation
27 -------------------------------
28 b09 (0.050): +0.306
29 b08 (0.050): +0.236
30 c01 (0.167): +0.194
31 b07 (0.050): +0.194
32 c02 (0.167): +0.167
33 b04 (0.050): +0.167
34 b03 (0.050): +0.167
35 b01 (0.050): +0.153
36 b05 (0.050): +0.056
37 b02 (0.050): +0.014
38 b06 (0.050): -0.042
39 c03 (0.167): -0.111
40 b10 (0.050): -0.111
41 Summary:
42 Weighted mean marginal correlation (a): +0.099
43 Standard deviation (b) : +0.132
44 Ranking fairness (a)-(b) : -0.033
```

Both Kemeny rankings show the same *weighted mean marginal correlation* (+0.099, see Listing 1.46 Lines 19-22, 42-44) with all thirteen performance criteria. However, the second ranking shows a slightly lower *standard deviation* (+0.132 vs +0.177), resulting in a slightly **fairer** ranking result (-0.033 vs -0.079).

When several rankings with maximal Kemeny index are given, the `KemenyRanking`

class constructor instantiates a *most consensual* one, i.e. a ranking with *highest* mean marginal correlation and, in case of ties, with *lowest* weighted standard deviation. Here we obtain ranking: [‘a5’, ‘a6’, ‘a7’, ‘a3’, ‘a9’, ‘a4’, ‘a1’, ‘a8’, ‘a2’] (see Listing 1.43 Line 4).

### 1.8.5. *Slater* rankings

The **Slater** ranking rule is identical to *Kemeny*’s, but it is working, instead, on the *median cut polarised* digraph. *Slater*’s ranking rule is also *invariant* under the *codual* transform and delivers again indifferently on *g* or *gcd* the following results.

```
1>>> from linearOrders import SlaterRanking
2>>> sl = SlaterRanking(gcd,orderLimit=9)
3>>> sl.slaterRanking
4 ['a5', 'a6', 'a4', 'a1', 'a3', 'a7', 'a8', 'a9', 'a2']
5>>> corr = gcd.computeOrderCorrelation(sl.slaterRanking)
6>>> sl.showCorrelation(corr)
7 Correlation indexes:
8 Extended Kendall tau : +0.676
9 Epistemic determination : 0.230
10 Bipolar-valued equivalence : +0.156
11>>> len(sl.maximalRankings)
12 7
```

We notice in Listing 1.47 Line 7 that the first *Slater* ranking is a rather good fit (+0.676), slightly better apparently than the *NetFlows* ranking result (+638). However, there are in fact 7 such potentially optimal *Slater* rankings (see Listing 1.47 Line 11). The corresponding epistemic disjunction gives the following partial ordering.

```
1>>> slw = RankingsFusion(sl,sl.maximalRankings)
2>>> slw.exportGraphViz(fileName='tutorialSlater')
3 *---- exporting a dot file for GraphViz tools ---------*
4 Exporting to tutorialSlater.dot
5 0 { rank = same; a5; }
6 1 { rank = same; a6; }
7 2 { rank = same; a7; a4; }
8 3 { rank = same; a1; }
9 4 { rank = same; a8; a3; }
10 5 { rank = same; a9; }
11 6 { rank = same; a2; }
12 dot -Grankdir=TB -Tpng tutorialSlater.dot -o tutorialSlater.png
```

What precise ranking result should we hence adopt? *Kemeny*’s and *Slater*’s ranking rules are furthermore computationally *difficult* problems and effective ranking results are only computable for tiny outranking digraphs (< 20 objects).

More efficient ranking heuristics, like the *Copeland* and the *NetFlows* rules, are therefore needed in practice. Let us finally, after these *ranking-by-scoring* strategies, also present two popular *ranking-by-choosing* strategies.

### 1.8.6. *Kohler*’s ranking-by-choosing rule

**Kohler**’s *ranking-by-choosing* rule can be formulated like this.

At step *i* (*i* goes from 1 to *n*) do the following:

Compute for each row of the bipolar-valued

*strict*outranking relation table (see Listing 1.35) the smallest value;Select the row where this minimum is maximal. Ties are resolved in lexicographic order;

Put the selected decision alternative at rank

*i*;Delete the corresponding row and column from the relation table and restart until the table is empty.

```
1>>> from linearOrders import KohlerRanking
2>>> kocd = KohlerRanking(gcd)
3>>> kocd.showRanking()
4 ['a5', 'a7', 'a6', 'a3', 'a9', 'a8', 'a4', 'a1', 'a2']
5>>> corr = gcd.computeOrdinalCorrelation(kocd)
6>>> gcd.showCorrelation(corr)
7 Correlation indexes:
8 Extended Kendall tau : +0.747
9 Epistemic determination : 0.230
10 Bipolar-valued equivalence : +0.172
```

With this *min-max* lexicographic *ranking-by-choosing* strategy, we find a correlation result (+0.747) that is until now clearly the nearest to an optimal *Kemeny* ranking (see Listing 1.44). Only two adjacent pairs: *[a6, a7]* and *[a8, a9]* are actually inverted here. Notice that *Kohler*’s ranking rule, contrary to the previously mentioned rules, is **not** *invariant* under the *codual* transform and requires to work on the *strict outranking* digraph *gcd* for a better correlation result.

```
1>>> ko = KohlerRanking(g)
2>>> corr = g.computeOrdinalCorrelation(ko)
3>>> g.showCorrelation(corr)
4 Correlation indexes:
5 Crisp ordinal correlation : +0.483
6 Epistemic determination : 0.230
7 Bipolar-valued equivalence : +0.111
```

But *Kohler*’s ranking has a *dual* version, the prudent **Arrow-Raynaud** *ordering-by-choosing* rule, where a corresponding *max-min* strategy, when used on the *non-strict* outranking digraph *g*, for ordering the from *last* to *first* produces a similar ranking result (see [LAM-2009], [DIA-2010]).

Noticing that the *NetFlows* score of an alternative *x* represents in fact a bipolar-valued characteristic of the assertion ‘**alternative x is ranked first**’, we may enhance *Kohler*’s or *Arrow-Raynaud*’s rules by replacing the *min-max*, respectively the *max-min*, strategy with an **iterated** maximal, respectively its *dual* minimal, *Netflows* score selection.

For a ranking (resp. an ordering) result, at step *i* (*i* goes from 1 to *n*) do the following:

Compute for each row of the bipolar-valued outranking relation table (see Listing 1.35) the corresponding net flow score ;

Select the row where this score is maximal (resp. minimal); ties being resolved by lexicographic order;

Put the corresponding decision alternative at rank (resp. order)

*i*;Delete the corresponding row and column from the relation table and restart until the table is empty.

A first *advantage* is that the so modified *Kohler*’s and *Arrow-Raynaud*’s rules become **invariant** under the *codual* transform. And we may get both the *ranking-by-choosing* as well as the *ordering-by-choosing* results with the `IteratedNetFlowsRanking`

class constructor (see Listing 1.50 Lines 12-13).

```
1>>> from linearOrders import IteratedNetFlowsRanking
2>>> inf = IteratedNetFlowsRanking(g)
3>>> inf
4 *------- Digraph instance description ------*
5 Instance class : IteratedNetFlowsRanking
6 Instance name : rel_randomCBperftab_ranked
7 Digraph Order : 9
8 Digraph Size : 36
9 Valuation domain : [-1.00;1.00]
10 Determinateness (%) : 100.00
11 Attributes : ['valuedRanks', 'valuedOrdering',
12 'iteratedNetFlowsRanking',
13 'iteratedNetFlowsOrdering',
14 'name', 'actions', 'order',
15 'valuationdomain', 'relation',
16 'gamma', 'notGamma']
17>>> inf.iteratedNetFlowsOrdering
18 ['a2', 'a9', 'a1', 'a4', 'a3', 'a8', 'a7', 'a6', 'a5']
19>>> corr = g.computeOrderCorrelation(inf.iteratedNetFlowsOrdering)
20>>> g.showCorrelation(corr)
21 Correlation indexes:
22 Crisp ordinal correlation : +0.751
23 Epistemic determination : 0.230
24 Bipolar-valued equivalence : +0.173
25>>> inf.iteratedNetFlowsRanking
26 ['a5', 'a7', 'a6', 'a3', 'a4', 'a1', 'a8', 'a9', 'a2']
27>>> corr = g.computeRankingCorrelation(inf.iteratedNetFlowsRanking)
28>>> g.showCorrelation(corr)
29 Correlation indexes:
30 Crisp ordinal correlation : +0.743
31 Epistemic determination : 0.230
32 Bipolar-valued equivalence : +0.171
```

The iterated *NetFlows* ranking and its *dual*, the iterated *NetFlows* ordering, do not usually deliver both the same result (Listing 1.50 Lines 18 and 26). With our example outranking digraph *g* for instance, it is the *ordering-by-choosing* result that obtains a slightly better correlation with the given outranking digraph *g* (+0.751), a result that is also slightly better than *Kohler*’s original result (+0.747, see Listing 1.49 Line 8).

With different *ranking-by-choosing* and *ordering-by-choosing* results, it may be useful to *fuse* now, similar to what we have done before with *Kemeny*’s and *Slaters*’s optimal rankings (see Listing 1.45 and Listing 1.48), both, the iterated *NetFlows* ranking and ordering into a partial ranking. But we are hence back to the practical problem of what linear ranking should we eventually retain ?

Let us finally mention another interesting *ranking-by-choosing* approach.

### 1.8.7. *Tideman*’s ranked-pairs rule

*Tideman*’s *ranking-by-choosing* heuristic, the **RankedPairs** rule, working best this time on the non strict outranking digraph *g*, is based on a *prudent incremental* construction of linear orders that avoids on the fly any cycling outrankings (see [LAM-2009]). The ranking rule may be formulated as follows:

Rank the ordered pairs of alternatives in decreasing order of ;

Consider the pairs in that order (ties are resolved by a lexicographic rule):

if the next pair does not create a

*circuit*with the pairs already blocked, block this pair;if the next pair creates a

*circuit*with the already blocked pairs, skip it.

With our didactic outranking digraph *g*, we get the following result.

```
1>>> from linearOrders import RankedPairsRanking
2>>> rp = RankedPairsRanking(g)
3>>> rp.showRanking()
4 ['a5', 'a6', 'a7', 'a3', 'a8', 'a9', 'a4', 'a1', 'a2']
```

The *RankedPairs* ranking rule renders in our example here luckily one of the two optimal *Kemeny* ranking, as we may verify below.

```
1>>> ke.maximalRankings
2 [['a5', 'a6', 'a7', 'a3', 'a8', 'a9', 'a4', 'a1', 'a2'],
3 ['a5', 'a6', 'a7', 'a3', 'a9', 'a4', 'a1', 'a8', 'a2']]
4>>> corr = g.computeOrdinalCorrelation(rp)
5>>> g.showCorrelation(corr)
6 Correlation indexes:
7 Extended Kendall tau : +0.779
8 Epistemic determination : 0.230
9 Bipolar-valued equivalence : +0.179
```

Similar to *Kohler*’s rule, the *RankedPairs* rule has also a prudent *dual* version, the **Dias-Lamboray** *ordering-by-choosing* rule, which produces, when working this time on the codual *strict outranking* digraph *gcd*, a similar ranking result (see [LAM-2009], [DIA-2010]).

Besides of not providing a unique linear ranking, the *ranking-by-choosing* rules, as well as their dual *ordering-by-choosing* rules, are unfortunately *not scalable* to outranking digraphs of larger orders (> 100). For such bigger outranking digraphs, with several hundred or thousands of alternatives, only the *Copeland*, the *NetFlows* ranking-by-scoring rules, with a polynomial complexity of , where *n* is the order of the outranking digraph, remain in fact computationally tractable.

Back to Content Table

## 1.9. The best academic *Computer Science* Depts: a *ranking* case study

In this tutorial, we are studying a ranking decision problem based on published data from the *Times Higher Education* (THE) *World University Rankings* 2016 by *Computer Science* (CS) subject 36. Several hundred academic CS Departments, from all over the world, were ranked that year following an overall numerical score based on the weighted average of five performance criteria: *Teaching* (the learning environment, 30%), *Research* (volume, income and reputation, 30%), *Citations* (research influence, 27.5%), *International outlook* (staff, students, and research, 7.5%), and *Industry income* (innovation, 5%).

To illustrate our *Digraph3* programming resources, we shall first have a look into the THE ranking data with short Python scripts. In a second Section, we shall relax the commensurability hypothesis of the ranking criteria and show how to similarly rank with multiple incommensurable performance criteria of ordinal significance. A third Section is finally devoted to introduce quality measures for qualifying ranking results.

### 1.9.1. The THE performance tableau

For our tutorial purpose, an extract of the published THE University rankings 2016 by computer science subject data, concerning the 75 first-ranked academic Institutions, is stored in a file named the_cs_2016.py of `PerformanceTableau`

format 37.

```
1>>> from perfTabs import PerformanceTableau
2>>> t = PerformanceTableau('the_cs_2016')
3>>> t
4 *------- PerformanceTableau instance description ------*
5 Instance class : PerformanceTableau
6 Instance name : the_cs_2016
7 # Actions : 75
8 # Objectives : 5
9 # Criteria : 5
10 NaN proportion (%) : 0.0
11 Attributes : ['name', 'description', 'actions',
12 'objectives', 'criteria',
13 'weightPreorder', 'NA', 'evaluation']
```

Potential *decision actions*, in our case here, are the 75 THE best-ranked *CS Departments*, all of them located at world renowned Institutions, like *California Institute of Technology*, *Swiss Federal Institute of Technology Zurich*, *Technical University München*, *University of Oxford* or the *National University of Singapore* (see Listing 1.53 below).

Instead of using prefigured *Digraph3* **show** methods, readily available for inspecting *PerformanceTableau* instances, we will illustrate below how to write small Python scripts for printing out its content.

```
1>>> for x in t.actions:
2... print('%s:\t%s (%s)' %\
3... (x,t.actions[x]['name'],t.actions[x]['comment']) )
4
5 albt: University of Alberta (CA)
6 anu: Australian National University (AU)
7 ariz: Arizona State University (US)
8 bju: Beijing University (CN)
9 bro: Brown University (US)
10 calt: California Institute of Technology (US)
11 cbu: Columbia University (US)
12 chku: Chinese University of Hong Kong (HK)
13 cihk: City University of Hong Kong (HK)
14 cir: University of California at Irvine (US)
15 cmel: Carnegie Mellon University (US)
16 cou: Cornell University (US)
17 csb: University of California at Santa Barbara (US)
18 csd: University Of California at San Diego (US)
19 dut: Delft University of Technology (NL)
20 eind: Eindhoven University of Technology (NL)
21 ens: Superior Normal School at Paris (FR)
22 epfl: Swiss Federal Institute of Technology Lausanne (CH)
23 epfr: Polytechnic school of Paris (FR)
24 ethz: Swiss Federal Institute of Technology Zurich (CH)
25 frei: University of Freiburg (DE)
26 git: Georgia Institute of Technology (US)
27 glas: University of Glasgow (UK)
28 hels: University of Helsinki (FI)
29 hkpu: Hong Kong Polytechnic University (CN)
30 hkst: Hong Kong University of Science and Technology (HK)
31 hku: Hong Kong University (HK)
32 humb: Berlin Humboldt University (DE)
33 icl: Imperial College London (UK)
34 indis: Indian Institute of Science (IN)
35 itmo: ITMO University (RU)
36 kcl: King's College London (UK)
37 kist: Korea Advances Institute of Science and Technology (KR)
38 kit: Karlsruhe Institute of Technology (DE)
39 kth: KTH Royal Institute of Technology (SE)
40 kuj: Kyoto University (JP)
41 kul: Catholic University Leuven (BE)
42 lms: Lomonosov Moscow State University (RU)
43 man: University of Manchester (UK)
44 mcp: University of Maryland College Park (US)
45 mel: University of Melbourne (AU)
46 mil: Polytechnic University of Milan (IT)
47 mit: Massachusetts Institute of Technology (US)
48 naji: Nanjing University (CN)
49 ntu: Nanyang Technological University of Singapore (SG)
50 ntw: National Taiwan University (TW)
51 nyu: New York University (US)
52 oxf: University of Oxford (UK)
53 pud: Purdue University (US)
54 qut: Queensland University of Technology (AU)
55 rcu: Rice University (US)
56 rwth: RWTH Aachen University (DE)
57 shJi: Shanghai Jiao Tong University (CN)
58 sing: National University of Singapore (SG)
59 sou: University of Southhampton (UK)
60 stut: University of Stuttgart (DE)
61 tech: Technion - Israel Institute of Technology (IL)
62 tlavu: Tel Aviv University (IR)
63 tsu: Tsinghua University (CN)
64 tub: Technical University of Berlin (DE)
65 tud: Technical University of Darmstadt (DE)
66 tum: Technical University of München (DE)
67 ucl: University College London (UK)
68 ued: University of Edinburgh (UK)
69 uiu: University of Illinois at Urbana-Champagne (US)
70 unlu: University of Luxembourg (LU)
71 unsw: University of New South Wales (AU)
72 unt: University of Toronto (CA)
73 uta: University of Texas at Austin (US)
74 utj: University of Tokyo (JP)
75 utw: University of Twente (NL)
76 uwa: University of Waterloo (CA)
77 wash: University of Washington (US)
78 wtu: Vienna University of Technology (AUS)
79 zhej: Zhejiang University (CN)
```

The THE authors base their ranking decisions on five objectives.

```
1>>> for obj in t.objectives:
2... print('%s: %s (%.1f%%),\n\t%s' %\
3... (obj,t.objectives[obj]['name'],\
4... t.objectives[obj]['weight'],
5... t.objectives[obj]['comment'])\
6... )
7
8 Teaching: Best learning environment (30.0%),
9 Reputation survey; Staff-to-student ration;
10 Doctorate-to-student ratio,
11 Doctorate-to-academic-staff ratio, Institutional income.
12 Research: Highest volume and repustation (30.0%),
13 Reputation survey; Research income; Research productivity
14 Citations: Highest research influence (27.5%),
15 Impact.
16 International outlook: Most international staff, students and research (7.5%),
17 Proportions of international students; of international staff;
18 international collaborations.
19 Industry income: Best knowledge transfer (5.0%),
20 Volume.
```

With a cumulated importance of 87% (see above), *Teaching*, *Research* and *Citations* represent clearly the **major** ranking objectives. *International outlook* and *Industry income* are considered of **minor** importance (12.5%).

THE does, unfortunately, not publish the detail of their performance assessments for grading CS Depts with respect to each one of the five ranking objectives 39. The THE 2016 ranking publication reveals solely a compound assessment on a single *performance criteria* per ranking objective. The five retained performance criteria may be printed out as follows.

```
1>>> for g in t.criteria:
2... print('%s:\t%s, %s (%.1f%%)' %\
3... (g,t.criteria[g]['name'],t.criteria[g]['comment'],\
4... t.criteria[g]['weight']) )
5
6 gtch: Teaching, The learning environment (30.0%)
7 gres: Research, Volume, income and reputation (30.0%)
8 gcit: Citations, Research influence (27.5%)
9 gint: International outlook, In staff, students and research (7.5%)
10 gind: Industry income, knowledge transfer (5.0%)
```

The largest part (87.5%) of criteria significance is, hence canonically, allocated to the major ranking criteria: *Teaching* (30%), *Research* (30%) and *Citations* (27.5%). The small remaining part (12.5%) goes to *International outlook* (7.5%) and *Industry income* (5%).

In order to render commensurable these performance criteria, the THE authors replace, per criterion, the actual performance grade obtained by each University with the corresponding **quantile** observed in the *cumulative distribution* of the performance grades obtained by all the surveyed institutions 40. The THE ranking is eventually determined by an **overall score** per University which corresponds to the **weighted average** of these five criteria quantiles (see Listing 1.54 below).

```
1>>> theScores = []
2>>> for x in t.actions:
3... xscore = Decimal('0')
4... for g in t.criteria:
5... xscore += t.evaluation[g][x] *\
6... (t.criteria[g]['weight']/Decimal('100'))
7... theScores.append((xscore,x))
```

In Listing 1.55 Lines 15-16 below, we may thus notice that, in the 2016 edition of the *THE World University rankings* by CS subject, the *Swiss Federal Institute of Technology Zürich* is first-ranked with an overall score of 92.9; followed by the *California Institute of Technology* (overall score: 92.4) 38.

```
1>>> theScores.sort(reverse = True)
2>>> print('## Univ \tgtch gres gcit gint gind overall')
3>>> print('-------------------------------------------------')
4>>> i = 1
5>>> for it in theScores:
6... x = it[1]
7... xScore = it[0]
8... print('%2d: %s' % (i,x), end=' \t')
9... for g in t.criteria:
10... print('%.1f ' % (t.evaluation[g][x]),end=' ')
11... print(' %.1f' % xScore)
12... i += 1
13
14 ## Univ gtch gres gcit gint gind overall
15 -------------------------------------------------
16 1: ethz 89.2 97.3 97.1 93.6 64.1 92.9
17 2: calt 91.5 96.0 99.8 59.1 85.9 92.4
18 3: oxf 94.0 92.0 98.8 93.6 44.3 92.2
19 4: mit 87.3 95.4 99.4 73.9 87.5 92.1
20 5: git 87.2 99.7 91.3 63.0 79.5 89.9
21 6: cmel 88.1 92.3 99.4 58.9 71.1 89.4
22 7: icl 90.1 87.5 95.1 94.3 49.9 89.0
23 8: epfl 86.3 91.6 94.8 97.2 42.7 88.9
24 9: tum 87.6 95.1 87.9 52.9 95.1 87.7
25 10: sing 89.9 91.3 83.0 95.3 50.6 86.9
26 11: cou 81.6 94.1 99.7 55.7 45.7 86.6
27 12: ucl 85.5 90.3 87.6 94.7 42.4 86.1
28 13: wash 84.4 88.7 99.3 57.4 41.2 85.6
29 14: hkst 74.3 92.0 96.2 84.4 55.8 85.5
30 15: ntu 76.6 87.7 90.4 92.9 86.9 85.5
31 16: ued 85.7 85.3 89.7 95.0 38.8 85.0
32 17: unt 79.9 84.4 99.6 77.6 38.4 84.4
33 18: uiu 85.0 83.1 99.2 51.4 42.2 83.7
34 19: mcp 79.7 89.3 94.6 29.8 51.7 81.5
35 20: cbu 81.2 78.5 94.7 66.9 45.7 81.3
36 21: tsu 88.1 90.2 76.7 27.1 85.9 80.9
37 22: csd 75.2 81.6 99.8 39.7 59.8 80.5
38 23: uwa 75.3 82.6 91.3 72.9 41.5 80.0
39 24: nyu 71.1 77.4 99.4 78.0 39.8 79.7
40 25: uta 72.6 85.3 99.6 31.6 49.7 79.6
41 26: kit 73.8 85.5 84.4 41.3 76.8 77.9
42 27: bju 83.0 85.3 70.1 30.7 99.4 77.0
43 28: csb 65.6 70.9 94.8 72.9 74.9 76.2
44 29: rwth 77.8 85.0 70.8 43.7 89.4 76.1
45 30: hku 77.0 73.0 77.0 96.8 39.5 75.4
46 31: pud 76.9 84.8 70.8 58.1 56.7 75.2
47 32: kist 79.4 88.2 64.2 31.6 92.8 74.9
48 33: kcl 45.5 94.6 86.3 95.1 38.3 74.8
49 34: chku 64.1 69.3 94.7 75.6 49.9 74.2
50 35: epfr 81.7 60.6 78.1 85.3 62.9 73.7
51 36: dut 64.1 78.3 76.3 69.8 90.1 73.4
52 37: tub 66.2 82.4 71.0 55.4 99.9 73.3
53 38: utj 92.0 91.7 48.7 25.8 49.6 72.9
54 39: cir 68.8 64.6 93.0 65.1 40.4 72.5
55 40: ntw 81.5 79.8 66.6 25.5 67.6 72.0
56 41: anu 47.2 73.0 92.2 90.0 48.1 70.6
57 42: rcu 64.1 53.8 99.4 63.7 46.1 69.8
58 43: mel 56.1 70.2 83.7 83.3 50.4 69.7
59 44: lms 81.5 68.1 61.0 31.1 87.8 68.4
60 45: ens 71.8 40.9 98.7 69.6 43.5 68.3
61 46: wtu 61.8 73.5 73.7 51.9 62.2 67.9
62 47: tech 54.9 71.0 85.1 51.7 40.1 67.1
63 48: bro 58.5 54.9 96.8 52.3 38.6 66.5
64 49: man 63.5 71.9 62.9 84.1 42.1 66.3
65 50: zhej 73.5 70.4 60.7 22.6 75.7 65.3
66 51: frei 54.2 51.6 89.5 49.7 99.9 65.1
67 52: unsw 60.2 58.2 70.5 87.0 44.3 63.6
68 53: kuj 75.4 72.8 49.5 28.3 51.4 62.8
69 54: sou 48.2 60.7 75.5 87.4 43.2 62.1
70 55: shJi 66.9 68.3 62.4 22.8 38.5 61.4
71 56: itmo 58.0 32.0 98.7 39.2 68.7 60.5
72 57: kul 35.2 55.8 92.0 46.0 88.3 60.5
73 58: glas 35.2 52.5 91.2 85.8 39.2 59.8
74 59: utw 38.2 52.8 87.0 69.0 60.0 59.4
75 60: stut 54.2 60.6 61.1 36.3 97.8 58.9
76 61: naji 51.4 76.9 48.8 39.7 74.4 58.6
77 62: tud 46.6 53.6 75.9 53.7 66.5 58.3
78 63: unlu 35.2 44.2 87.4 99.7 54.1 58.0
79 64: qut 45.5 42.6 82.8 75.2 63.0 58.0
80 65: hkpu 46.8 36.5 91.4 73.2 41.5 57.7
81 66: albt 39.2 53.3 69.9 91.9 75.4 57.6
82 67: mil 46.4 64.3 69.2 44.1 38.5 57.5
83 68: hels 48.8 49.6 80.4 50.6 39.5 57.4
84 69: cihk 42.4 44.9 80.1 76.2 67.9 57.3
85 70: tlavu 34.1 57.2 89.0 45.3 38.6 57.2
86 71: indis 56.9 76.1 49.3 20.1 41.5 57.0
87 72: ariz 28.4 61.8 84.3 59.3 42.0 56.8
88 73: kth 44.8 42.0 83.6 71.6 39.2 56.4
89 74: humb 48.4 31.3 94.7 41.5 45.5 55.3
90 75: eind 32.4 48.4 81.5 72.2 45.8 54.4
```

It is important to notice that a ranking by weighted average scores requires *commensurable ranking criteria* of *precise decimal significance* and on wich a *precise decimal performance grading* is given. It is very unlikely that the THE 2016 performance assessments indeed verify these conditions. This tutorial shows how to relax these methodological requirements -precise commensurable criteria and numerical assessments- by following instead an epistemic bipolar-valued logic based ranking methodology.

### 1.9.2. Ranking with multiple incommensurable criteria of ordinal significance

Let us, first, have a critical look at the THE performance criteria.

```
>>> t.showHTMLCriteria(Sorted=False)
```

Considering a very likely imprecision of the performance grading procedure, followed by some potential violation of uniform distributed quantile classes, we assume here that a performance quantile difference of up to **abs(2.5)%** is **insignificant**, whereas a difference of **abs(5)%** warrants a **clearly better**, resp. **clearly less good**, performance. With quantiles 94%, resp. 87.3%, *Oxford*’s CS teaching environment, for instance, is thus clearly better evaluated than that of the *MIT* (see Listing 1.54 Lines 27-28). We shall furthermore assume that a **considerable** performance quantile difference of **abs(60)%**, observed on the three major ranking criteria: *Teaching*, *Research* and *Citations*, will trigger a **veto**, respectively a **counter-veto** against a *pairwise outranking*, respectively a *pairwise outranked* situation [BIS-2013].

The effect of these performance discrimination thresholds on the preference modelling may be inspected as follows.

```
1>>> t.showCriteria()
2 *---- criteria -----*
3 gtch 'Teaching'
4 Scale = (Decimal('0.00'), Decimal('100.00'))
5 Weight = 0.300
6 Threshold ind : 2.50 + 0.00x ; percentile: 8.07
7 Threshold pref : 5.00 + 0.00x ; percentile: 15.75
8 Threshold veto : 60.00 + 0.00x ; percentile: 99.75
9 gres 'Research'
10 Scale = (Decimal('0.00'), Decimal('100.00'))
11 Weight = 0.300
12 Threshold ind : 2.50 + 0.00x ; percentile: 7.86
13 Threshold pref : 5.00 + 0.00x ; percentile: 16.14
14 Threshold veto : 60.00 + 0.00x ; percentile: 99.21
15 gcit 'Citations'
16 Scale = (Decimal('0.00'), Decimal('100.00'))
17 Weight = 0.275
18 Threshold ind : 2.50 + 0.00x ; percentile: 11.82
19 Threshold pref : 5.00 + 0.00x ; percentile: 22.99
20 Threshold veto : 60.00 + 0.00x ; percentile: 100.00
21 gint 'International outlook'
22 Scale = (Decimal('0.00'), Decimal('100.00'))
23 Weight = 0.075
24 Threshold ind : 2.50 + 0.00x ; percentile: 6.45
25 Threshold pref : 5.00 + 0.00x ; percentile: 11.75
26 gind 'Industry income'
27 Scale = (Decimal('0.00'), Decimal('100.00'))
28 Weight = 0.050
29 Threshold ind : 2.50 + 0.00x ; percentile: 11.82
30 Threshold pref : 5.00 + 0.00x ; percentile: 21.51
```

Between 6% and 12% of the observed quantile differences are, thus, considered to be *insignificant*. Similarly, between 77% and 88% are considered to be *significant*. Less than 1% correspond to *considerable* quantile differences on both the *Teaching* and *Research* criteria; actually triggering an epistemic *polarisation* effect [BIS-2013].

Beside the likely imprecise performance discrimination, the **precise decimal** significance weights, as allocated by the THE authors to the five ranking criteria (see Fig. 1.24 Column *Weight*) are, as well, quite **questionable**. Significance weights may carry usually hidden strategies for rendering the performance evaluations commensurable in view of a numerical computation of the overall ranking scores. The eventual ranking result is thus as much depending on the precise values of the given criteria significance weights as, vice versa, the given precise significance weights are depending on the subjectively expected and accepted ranking results 42. We will therefore drop such precise weights and, instead, only require a corresponding criteria significance preorder: *gtch* = *gres* > *gcit* > *gint* > *gind*. *Teaching environment* and *Research volume and reputation* are equally considered most important, followed by *Research influence*. Than comes *International outlook in staff, students and research* and, least important finally, *Industry income and innovation*.

Both these working hypotheses: performance *discrimitation* thresholds and solely *ordinal* criteria significance, give us way to a ranking methodology based on **robust pairwise outranking** situations [BIS-2004b]:

We say that CS Dept

xrobustly outranksCS Deptywhenxpositively outranksywithallsignificance weight vectors that arecompatiblewith thesignificance preorder:gtch=gres>gcit>gint>gind;We say that CS Dept

xisrobustly outrankedby CS Deptywhenxis positively outranked byywithallsignificance weight vectors that arecompatiblewith thesignificance preorder:gtch=gres>gcit>gint>gind;Otherwise, CS Depts

xandyare considered to beincomparable.

A corresponding digraph constructor is provided by the `RobustOutrankingDigraph`

class.

```
1>>> from outrankingDigraphs import RobustOutrankingDigraph
2>>> rdg = RobustOutrankingDigraph(t)
3>>> rdg
4 *------- Object instance description ------*
5 Instance class : RobustOutrankingDigraph
6 Instance name : robust_the_cs_2016
7 # Actions : 75
8 # Criteria : 5
9 Size : 2993
10 Determinateness (%) : 78.16
11 Valuation domain : [-1.00;1.00]
12>>> rdg.computeIncomparabilityDegree(Comments=True)
13 Incomparability degree (%) of digraph <robust_the_cs_2016>:
14 #links x<->y y: 2775, #incomparable: 102, #comparable: 2673
15 (#incomparable/#links) = 0.037
16>>> rdg.computeTransitivityDegree(Comments=True)
17 Transitivity degree of digraph <robust_the_cs_2016>:
18 #triples x>y>z: 405150, #closed: 218489, #open: 186661
19 (#closed/#triples) = 0.539
20>>> rdg.computeSymmetryDegree(Comments=True)
21 Symmetry degree (%) of digraph <robust_the_cs_2016>:
22 #arcs x>y: 2673, #symmetric: 320, #asymmetric: 2353
23 (#symmetric/#arcs) = 0.12
```

In the resulting digraph instance *rdg* (see Listing 1.57 Line 8), we observe 2993 such **robust pairwise outranking** situations validated with a mean significance of 78% (Line 9). Unfortunately, in our case here, they do not deliver any complete linear ranking relation. The robust outranking digraph *rdg* contains in fact 102 incomparability situations (3.7%, Line 13); nearly half of its transitive closure is missing (46.1%, Line 18) and 12% of the positive outranking situations correspond in fact to symmetric *indifference* situations (Line 22).

Worse even, the digraph *rdg* admits furthermore a high number of outranking circuits.

```
1>>> rdg.computeChordlessCircuits()
2>>> rdg.showChordlessCircuits()
3 *---- Chordless circuits ----*
4 145 circuits.
5 1: ['albt', 'unlu', 'ariz', 'hels'] , credibility : 0.300
6 2: ['albt', 'tlavu', 'hels'] , credibility : 0.150
7 3: ['anu', 'man', 'itmo'] , credibility : 0.250
8 4: ['anu', 'zhej', 'rcu'] , credibility : 0.250
9 ...
10 ...
11 82: ['csb', 'epfr', 'rwth'] , credibility : 0.250
12 83: ['csb', 'epfr', 'pud', 'nyu'] , credibility : 0.250
13 84: ['csd', 'kcl', 'kist'] , credibility : 0.250
14 ...
15 ...
16 142: ['kul', 'qut', 'mil'] , credibility : 0.250
17 143: ['lms', 'rcu', 'tech'] , credibility : 0.300
18 144: ['mil', 'stut', 'qut'] , credibility : 0.300
19 145: ['mil', 'stut', 'tud'] , credibility : 0.300
```

Among the 145 detected robust outranking circuits reported in Listing 1.58, we notice, for instance, two outranking circuits of length 4 (see circuits #1 and #83). Let us explore below the bipolar-valued robust outranking characteristics of the first circuit.

```
1>>> rdg.showRelationTable(actionsSubset= ['albt','unlu','ariz','hels'],\
2... Sorted=False)
3
4 * ---- Relation Table -----
5 r/(stab)| 'albt' 'unlu' 'ariz' 'hels'
6 -----|------------------------------------------------------------
7 'albt' | +1.00 +0.30 +0.00 +0.00
8 | (+4) (+2) (-1) (-1)
9 'unlu' | +0.00 +1.00 +0.40 +0.00
10 | (+0) (+4) (+2) (-1)
11 'ariz' | +0.00 -0.12 +1.00 +0.40
12 | (+1) (-2) (+4) (+2)
13 'hels' | +0.45 +0.00 -0.03 +1.00
14 | (+2) (+1) (-2) (+4)
15 Valuation domain: [-1.0; 1.0]
16 Stability denotation semantics:
17 +4|-4 : unanimous outranking | outranked situation;
18 +2|-2 : outranking | outranked situation validated
19 with all potential significance weights that are
20 compatible with the given significance preorder;
21 +1|-1 : validated outranking | outranked situation with
22 the given significance weights;
23 0 : indeterminate relational situation.
```

In Listing 1.59, we may notice that the robust outranking circuit [‘albt’, ‘unlu’, ‘ariz’, ‘hels’] will reappear with all potential criteria significance weight vectors that are compatible with given preorder: *gtch* = *gres* > *gcit* > *gint* > *gind*. Notice also the (+1|-1) marked outranking situations, like the one between ‘albt’ and ‘ariz’. The statement that “*Arizona State University* strictly outranks *University of Alberta*” is in fact valid with the precise THE weight vector, but not with all potential weight vectors compatible with the given significance preorder. All these outranking situations are hence put into **doubt** () and the corresponding CS Depts, like *University of Alberta* and *Arizona State University*, become **incomparable** in a *robust outranking* sense.

Showing many incomparabilities and indifferences; not being transitive and containing many robust outranking circuits; all these relational characteristics, make that no ranking algorithm, applied to digraph *rdg*, does exist that would produce a *unique* optimal linear ranking result. Methodologically, we are only left with *ranking heuristics*. In the previous tutorial on ranking with multiple criteria we have seen now several potential heuristic ranking rules that may be applied to rank from a pairwise outranking digraph; yet, delivering all potentially more or less diverging results. Considering the order of digraph *rdg* (75) and the largely unequal THE criteria significance weights, we rather opt, in this tutorial, for the NetFlows ranking rule 41. Its complexity in is indeed quite tractable and, by avoiding potential *tyranny of short majority* effects, the *NetFlows* rule specifically takes the ranking criteria significance into a more fairly balanced account.

The *NetFlows* ranking result of the CS Depts may be computed explicitly as follows.

```
1>>> nfRanking = rdg.computeNetFlowsRanking()
2>>> nfRanking
3 ['ethz', 'calt', 'mit', 'oxf', 'cmel', 'git', 'epfl',
4 'icl', 'cou', 'tum', 'wash', 'sing', 'hkst', 'ucl',
5 'uiu', 'unt', 'ued', 'ntu', 'mcp', 'csd', 'cbu',
6 'uta', 'tsu', 'nyu', 'uwa', 'csb', 'kit', 'utj',
7 'bju', 'kcl', 'chku', 'kist', 'rwth', 'pud', 'epfr',
8 'hku', 'rcu', 'cir', 'dut', 'ens', 'ntw', 'anu',
9 'tub', 'mel', 'lms', 'bro', 'frei', 'wtu', 'tech',
10 'itmo', 'zhej', 'man', 'kuj', 'kul', 'unsw', 'glas',
11 'utw', 'unlu', 'naji', 'sou', 'hkpu', 'qut', 'humb',
12 'shJi', 'stut', 'tud', 'tlavu', 'cihk', 'albt', 'indis',
13 'ariz', 'kth', 'hels', 'eind', 'mil']
```

We actually obtain a very similar ranking result as the one obtained with the THE overall scores. The same group of seven Depts: *ethz*, *calt*, *mit*, *oxf*, *cmel*, *git* and *epfl*, is top-ranked. And a same group of Depts: *tlavu*, *cihk*, *indis*, *ariz*, *kth*, *‘hels*, *eind*, and *mil* appears at the end of the list.

We may print out the difference between the *overall scores* based THE ranking and our *NetFlows* ranking with the following short Python script, where we make use of an ordered Python dictionary with *net flow scores*, stored in the *rdg.netFlowsRankingDict* attribute by the previous computation.

```
1>>> # rdg.netFlowsRankingDict: ordered dictionary with net flow
2>>> # scores stored in rdg by the computeNetFlowsRanking() method
3>>> # theScores = [(xScore_1,x_1), (xScore_2,x_2),... ]
4>>> # is sorted in decreasing order of xscores_i
5>>> print(\
6... ' NetFlows ranking gtch gres gcit gint gind THE ranking')
7
8>>> for i in range(75):
9... x = nfRanking[i]
10... xScore = rdg.netFlowsRankingDict[x]['netFlow']
11... thexScore,thex = theScores[i]
12... print('%2d: %s (%.2f) ' % (i+1,x,xScore), end=' \t')
13... for g in rdg.criteria:
14... print('%.1f ' % (t.evaluation[g][x]),end=' ')
15... print(' %s (%.2f)' % (thex,thexScore) )
16
17 NetFlows ranking gtch gres gcit gint gind THE ranking
18 1: ethz (116.95) 89.2 97.3 97.1 93.6 64.1 ethz (92.88)
19 2: calt (116.15) 91.5 96.0 99.8 59.1 85.9 calt (92.42)
20 3: mit (112.72) 87.3 95.4 99.4 73.9 87.5 oxf (92.20)
21 4: oxf (112.00) 94.0 92.0 98.8 93.6 44.3 mit (92.06)
22 5: cmel (101.60) 88.1 92.3 99.4 58.9 71.1 git (89.88)
23 6: git (93.40) 87.2 99.7 91.3 63.0 79.5 cmel (89.43)
24 7: epfl (90.88) 86.3 91.6 94.8 97.2 42.7 icl (89.00)
25 8: icl (90.62) 90.1 87.5 95.1 94.3 49.9 epfl (88.86)
26 9: cou (84.60) 81.6 94.1 99.7 55.7 45.7 tum (87.70)
27 10: tum (80.42) 87.6 95.1 87.9 52.9 95.1 sing (86.86)
28 11: wash (76.28) 84.4 88.7 99.3 57.4 41.2 cou (86.59)
29 12: sing (73.05) 89.9 91.3 83.0 95.3 50.6 ucl (86.05)
30 13: hkst (71.05) 74.3 92.0 96.2 84.4 55.8 wash (85.60)
31 14: ucl (66.78) 85.5 90.3 87.6 94.7 42.4 hkst (85.47)
32 15: uiu (64.80) 85.0 83.1 99.2 51.4 42.2 ntu (85.46)
33 16: unt (62.65) 79.9 84.4 99.6 77.6 38.4 ued (85.03)
34 17: ued (58.67) 85.7 85.3 89.7 95.0 38.8 unt (84.42)
35 18: ntu (57.88) 76.6 87.7 90.4 92.9 86.9 uiu (83.67)
36 19: mcp (54.08) 79.7 89.3 94.6 29.8 51.7 mcp (81.53)
37 20: csd (46.62) 75.2 81.6 99.8 39.7 59.8 cbu (81.25)
38 21: cbu (44.27) 81.2 78.5 94.7 66.9 45.7 tsu (80.91)
39 22: uta (43.27) 72.6 85.3 99.6 31.6 49.7 csd (80.45)
40 23: tsu (42.42) 88.1 90.2 76.7 27.1 85.9 uwa (80.02)
41 24: nyu (35.30) 71.1 77.4 99.4 78.0 39.8 nyu (79.72)
42 25: uwa (28.88) 75.3 82.6 91.3 72.9 41.5 uta (79.61)
43 26: csb (18.18) 65.6 70.9 94.8 72.9 74.9 kit (77.94)
44 27: kit (16.32) 73.8 85.5 84.4 41.3 76.8 bju (77.04)
45 28: utj (15.95) 92.0 91.7 48.7 25.8 49.6 csb (76.23)
46 29: bju (15.45) 83.0 85.3 70.1 30.7 99.4 rwth (76.06)
47 30: kcl (11.95) 45.5 94.6 86.3 95.1 38.3 hku (75.41)
48 31: chku (9.43) 64.1 69.3 94.7 75.6 49.9 pud (75.17)
49 32: kist (7.30) 79.4 88.2 64.2 31.6 92.8 kist (74.94)
50 33: rwth (5.00) 77.8 85.0 70.8 43.7 89.4 kcl (74.81)
51 34: pud (2.40) 76.9 84.8 70.8 58.1 56.7 chku (74.23)
52 35: epfr (-1.70) 81.7 60.6 78.1 85.3 62.9 epfr (73.71)
53 36: hku (-3.83) 77.0 73.0 77.0 96.8 39.5 dut (73.44)
54 37: rcu (-6.38) 64.1 53.8 99.4 63.7 46.1 tub (73.25)
55 38: cir (-8.20) 68.8 64.6 93.0 65.1 40.4 utj (72.92)
56 39: dut (-8.85) 64.1 78.3 76.3 69.8 90.1 cir (72.50)
57 40: ens (-8.97) 71.8 40.9 98.7 69.6 43.5 ntw (72.00)
58 41: ntw (-11.15) 81.5 79.8 66.6 25.5 67.6 anu (70.57)
59 42: anu (-11.50) 47.2 73.0 92.2 90.0 48.1 rcu (69.79)
60 43: tub (-12.20) 66.2 82.4 71.0 55.4 99.9 mel (69.67)
61 44: mel (-23.98) 56.1 70.2 83.7 83.3 50.4 lms (68.38)
62 45: lms (-25.43) 81.5 68.1 61.0 31.1 87.8 ens (68.35)
63 46: bro (-27.18) 58.5 54.9 96.8 52.3 38.6 wtu (67.86)
64 47: frei (-34.42) 54.2 51.6 89.5 49.7 99.9 tech (67.06)
65 48: wtu (-35.05) 61.8 73.5 73.7 51.9 62.2 bro (66.49)
66 49: tech (-37.95) 54.9 71.0 85.1 51.7 40.1 man (66.33)
67 50: itmo (-38.50) 58.0 32.0 98.7 39.2 68.7 zhej (65.34)
68 51: zhej (-43.70) 73.5 70.4 60.7 22.6 75.7 frei (65.08)
69 52: man (-44.83) 63.5 71.9 62.9 84.1 42.1 unsw (63.65)
70 53: kuj (-47.40) 75.4 72.8 49.5 28.3 51.4 kuj (62.77)
71 54: kul (-49.98) 35.2 55.8 92.0 46.0 88.3 sou (62.15)
72 55: unsw (-54.88) 60.2 58.2 70.5 87.0 44.3 shJi (61.35)
73 56: glas (-56.98) 35.2 52.5 91.2 85.8 39.2 itmo (60.52)
74 57: utw (-59.27) 38.2 52.8 87.0 69.0 60.0 kul (60.47)
75 58: unlu (-60.08) 35.2 44.2 87.4 99.7 54.1 glas (59.78)
76 59: naji (-60.52) 51.4 76.9 48.8 39.7 74.4 utw (59.40)
77 60: sou (-60.83) 48.2 60.7 75.5 87.4 43.2 stut (58.85)
78 61: hkpu (-62.05) 46.8 36.5 91.4 73.2 41.5 naji (58.61)
79 62: qut (-66.17) 45.5 42.6 82.8 75.2 63.0 tud (58.28)
80 63: humb (-68.10) 48.4 31.3 94.7 41.5 45.5 unlu (58.04)
81 64: shJi (-69.72) 66.9 68.3 62.4 22.8 38.5 qut (57.99)
82 65: stut (-69.90) 54.2 60.6 61.1 36.3 97.8 hkpu (57.69)
83 66: tud (-70.83) 46.6 53.6 75.9 53.7 66.5 albt (57.63)
84 67: tlavu (-71.50) 34.1 57.2 89.0 45.3 38.6 mil (57.47)
85 68: cihk (-72.20) 42.4 44.9 80.1 76.2 67.9 hels (57.40)
86 69: albt (-72.33) 39.2 53.3 69.9 91.9 75.4 cihk (57.33)
87 70: indis (-72.53) 56.9 76.1 49.3 20.1 41.5 tlavu (57.19)
88 71: ariz (-75.10) 28.4 61.8 84.3 59.3 42.0 indis (57.04)
89 72: kth (-77.10) 44.8 42.0 83.6 71.6 39.2 ariz (56.79)
90 73: hels (-79.55) 48.8 49.6 80.4 50.6 39.5 kth (56.36)
91 74: eind (-82.85) 32.4 48.4 81.5 72.2 45.8 humb (55.34)
92 75: mil (-83.67) 46.4 64.3 69.2 44.1 38.5 eind (54.36)
```

The first inversion we observe in Listing 1.61 (Lines 20-21) concerns *Oxford University* and the *MIT*, switching positions 3 and 4. Most inversions are similarly short and concern only switching very close positions in either way. There are some slightly more important inversions concerning, for instance, the *Hong Kong University* CS Dept, ranked into position 30 in the THE ranking and here in the position 36 (Line 53). The opposite situation may also happen; the *Berlin Humboldt University* CS Dept, occupying the 74th position in the THE ranking, advances in the *NetFlows* ranking to position 63 (Line 80).

In our bipolar-valued epistemic framework, the *NetFlows* score of any CS Dept *x* (see Listing 1.61) corresponds to the criteria significance support for the logical statement (*x* is *first*-ranked). Formally

r(

xisfirst-ranked)

Using the robust outranking characteristics of digraph *rdg*, we may thus explicitly compute, for instance, *ETH Zürich*’s score, denoted *nfx* below.

```
1>>> x = 'ethz'
2>>> nfx = Decimal('0')
3>>> for y in rdg.actions:
4... if x != y:
5... nfx += (rdg.relation[x][y] - rdg.relation[y][x])
```

```
1>>> print(x, nfx)
2 ethz 116.950
```

In Listing 1.61 (Line 18), we may now verify that *ETH Zürich* obtains indeed the highest *NetFlows* score, and gives, hence the **most credible** *first*-ranked CS Dept of the 75 potential candidates.

How may we now convince the reader, that our pairwise outranking based ranking result here appears more objective and trustworthy, than the classic value theory based THE ranking by overall scores?

### 1.9.3. How to judge the quality of a ranking result?

In a multiple criteria based ranking problem, inspecting pairwise marginal performance differences may give objectivity to global preferential statements. That a CS Dept *x* convincingly outranks Dept *y* may thus conveniently be checked. The *ETH Zürich* CS Dept is, for instance, first ranked before *Caltech*’s Dept in both previous rankings. Lest us check the preferential reasons.

```
1>>> rdg.showPairwiseOutrankings('ethz','calt')
2 *------------ pairwise comparisons ----*
3 Valuation in range: -100.00 to +100.00
4 Comparing actions : (ethz, calt)
5 crit. wght. g(x) g(y) diff | ind pref r() |
6 ------------------------------- ------------------------
7 gcit 27.50 97.10 99.80 -2.70 | 2.50 5.00 +0.00 |
8 gind 5.00 64.10 85.90 -21.80 | 2.50 5.00 -5.00 |
9 gint 7.50 93.60 59.10 +34.50 | 2.50 5.00 +7.50 |
10 gres 30.00 97.30 96.00 +1.30 | 2.50 5.00 +30.00 |
11 gtch 30.00 89.20 91.50 -2.30 | 2.50 5.00 +30.00 |
12 r(x >= y): +62.50
13 crit. wght. g(y) g(x) diff | ind pref r() |
14 ------------------------------- ------------------------
15 gcit 27.50 99.80 97.10 +2.70 | 2.50 5.00 +27.50 |
16 gind 5.00 85.90 64.10 +21.80 | 2.50 5.00 +5.00 |
17 gint 7.50 59.10 93.60 -34.50 | 2.50 5.00 -7.50 |
18 gres 30.00 96.00 97.30 -1.30 | 2.50 5.00 +30.00 |
19 gtch 30.00 91.50 89.20 +2.30 | 2.50 5.00 +30.00 |
20 r(y >= x): +85.00
```

A significant positive performance difference (+34.50), concerning the *International outlook* criterion (of 7,5% significance), may be observed in favour of the *ETH Zürich* Dept (Line 9 above). Similarly, a significant positive performance difference (+21.80), concerning the *Industry income* criterion (of 5% significance), may be observed, this time, in favour of the *Caltech* Dept. The former, larger positive, performance difference, observed on a more significant criterion, gives so far a first convincing argument of 12.5% significance for putting *ETH Zürich* first, before *Caltech*. Yet, the slightly positive performance difference (+2.70) between *Caltech* and *ETH Zürich* on the *Citations* criterion (of 27.5% significance) confirms an *at least as good as* situation in favour of the *Caltech* Dept.

The inverse negative performance difference (-2.70), however, is neither *significant* (< -5.00), nor insignificant (> -2.50), and does hence **neither confirm nor infirm** a *not at least as good as* situation in disfavour of *ETH Zürich*. We observe here a convincing argument of 27.5% significance for putting *Caltech* first, before *ETH Zürich*.

Notice finally, that, on the *Teaching* and *Research* criteria of total significance 60%, both Depts do, with performance differences < abs(2.50), one as well as the other. As these two major performance criteria necessarily support together always the highest significance with the imposed significance weight preorder: *gtch* = *gres* > *gcit* > *gint* > *gind*, both outranking situations get in fact globally confirmed at stability level *+2* (see the advanced topic on stable outrankings with multiple criteria of ordinal significance).

We may well illustrate all such *stable outranking* situations with a browser view of the corresponding robust relation map using our *NetFlows* ranking.

```
>>> rdg.showHTMLRelationMap(tableTitle='Robust Outranking Map',
... rankingRule='NetFlows')
```

In Fig. 1.25, **dark green**, resp. **light green** marked positions show *certainly*, resp. *positively* valid **outranking** situations, whereas **dark red**, resp. **light red** marked positions show *certainly*, respectively *positively* valid **outranked** situations. In the left upper corner we may verify that the five top-ranked Depts ([‘ethz’, ‘calt’, ‘oxf’, ‘mit’, ‘cmel’]) are indeed mutually outranking each other and thus are to be considered all *indifferent*. They are even robust *Condorcet* winners, i.e positively outranking all other Depts. We may by the way notice that no certainly valid outranking (dark green) and no certainly valid outranked situations (dark red) appear **below**, resp. **above** the principal diagonal; none of these are hence violated by our *netFlows* ranking.

The non reflexive **white** positions in the relation map, mark outranking or outranked situations that are **not robust** with respect to the given significance weight preorder. They are, hence, put into doubt and set to the *indeterminate* characteristic value **0**.

By measuring the **ordinal correlation** with the underlying pairwise *global* and *marginal* robust outranking situations, the **quality** of the robust *netFlows* ranking result may be formally evaluated 27.

```
1>>> corrnf = rdg.computeRankingCorrelation(nfRanking)
2>>> rdg.showCorrelation(corrnf)
3 Correlation indexes:
4 Crisp ordinal correlation : +0.901
5 Epistemic determination : 0.563
6 Bipolar-valued equivalence : +0.507
```

In Listing 1.63 (Line 4), we may notice that the *NetFlows* ranking result is indeed highly ordinally correlated (+0.901, in *Kendall*’s index *tau* sense) with the pairwise global robust outranking relation. Their bipolar-valued *relational equivalence* value (+0.51, Line 6) indicates a more than 75% criteria significance support.

We may as well check how the *netFlows* ranking rule is actually balancing the five ranking criteria.

```
1>>> rdg.showRankingConsensusQuality(nfRanking)
2 Criterion (weight): correlation
3 -------------------------------
4 gtch (0.300): +0.660
5 gres (0.300): +0.638
6 gcit (0.275): +0.370
7 gint (0.075): +0.155
8 gind (0.050): +0.101
9 Summary:
10 Weighted mean marginal correlation (a): +0.508
11 Standard deviation (b) : +0.187
12 Ranking fairness (a)-(b) : +0.321
```

The correlations with the marginal performance criterion rankings are nearly respecting the given significance weights preorder: *gtch* ~ *gres* > *gcit* > *gint* > *gind* (see above Lines 4-8). The mean *marginal correlation* is quite high (+0.51). Coupled with a low standard deviation (0.187), we obtain a rather fairly balanced ranking result (Lines 10-12).

We may also inspect the mutual correlation indexes observed between the marginal criterion robust outranking relations.

```
1>>> rdg.showCriteriaCorrelationTable()
2 Criteria ordinal correlation index
3 | gcit gind gint gres gtch
4 -----|------------------------------------------
5 gcit | +1.00 -0.11 +0.24 +0.13 +0.17
6 gind | +1.00 -0.18 +0.15 +0.15
7 gint | +1.00 +0.04 -0.00
8 gres | +1.00 +0.67
9 gtch | +1.00
```

Slightly contradictory (-0.11) appear the *Citations* and *Industrial income* criteria (Line 5 Column 3). Due perhaps to potential confidentiality clauses, it seams not always possible to publish industrially relevant research results in highly ranked journals. However, criteria *Citations* and *International outlook* show a slightly positive correlation (+0.24, Column 4), whereas the *International outlook* criterion shows no apparent correlation with both the major *Teaching* and *Research* criteria. The latter are however highly correlated (+0.67. Line 9 Column 6).

A *Principal Component Analysis* may well illustrate the previous findings.

```
>>> rdg.export3DplotOfCriteriaCorrelation(graphType='png')
```

In Fig. 1.26 (factors 1 and 2 plot) we may notice, first, that more than 80% of the total variance of the previous correlation table is explained by the apparent opposition between the marginal outrankings of criteria: *Teaching*, *Research* & *Industry income* on the left side, and the marginal outrankings of criteria: *Citations* & *international outlook* on the right side. Notice also in the left lower corner the nearly identical positions of the marginal outrankings of the major *Teaching* & *Research* criteria. In the factors 2 and 3 plot, about 30% of the total variance is captured by the opposition between the marginal outrankings of the *Teaching* & *Research* criteria and the marginal outrankings of the *Industrial income* criterion. Finally, in the factors 1 and 3 plot, nearly 15% of the total variance is explained by the opposition between the marginal outrankings of the *International outlook* criterion and the marginal outrankings of the *Citations* criterion.

It may, finally, be interesting to assess, similarly, the ordinal correlation of the THE overall scores based ranking with respect to our robust outranking situations.

```
1>>> # theScores = [(xScore_1,x_1), (xScore_2,x_2),... ]
2>>> # is sorted in decreasing order of xscores
3>>> theRanking = [item[1] for item in theScores]
4>>> corrthe = rdg.computeRankingCorrelation(theRanking)
5>>> rdg.showCorrelation(corrthe)
6 Correlation indexes:
7 Crisp ordinal correlation : +0.907
8 Epistemic determination : 0.563
9 Bipolar-valued equivalence : +0.511
10>>> rdg.showRankingConsensusQuality(theRanking)
11 Criterion (weight): correlation
12 -------------------------------
13 gtch (0.300): +0.683
14 gres (0.300): +0.670
15 gcit (0.275): +0.319
16 gint (0.075): +0.161
17 gind (0.050): +0.106
18 Summary:
19 Weighted mean marginal correlation (a): +0.511
20 Standard deviation (b) : +0.210
21 Ranking fairness (a)-(b) : +0.302
```

The THE ranking result is similarly correlated (+0.907, Line 7) with the pairwise global robust outranking relation. By its overall weighted scoring rule, the THE ranking induces marginal criterion correlations that are naturally compatible with the given significance weight preorder (Lines 13-17). Notice that the mean marginal correlation is of a similar value (+0.51, Line 19) as the *netFlows* ranking’s. Yet, its standard deviation is higher, which leads to a slightly less fair balancing of the three major ranking criteria.

To conclude, let us emphasize, that, without any commensurability hypothesis and by taking, furthermore, into account, first, the always present more or less imprecision of any performance grading and, secondly, solely ordinal criteria significance weights, we may obtain here with our robust outranking approach a very similar ranking result with more or less a same, when not better, preference modelling quality. A convincing heatmap view of the 25 first-ranked Institutions may be generated in the default system browser with following command.

```
1>>> rdg.showHTMLPerformanceHeatmap(
2... WithActionNames=True,\
3... outrankingModel='this',\
4... rankingRule='NetFlows',\
5... ndigits=1,\
6... Correlations=True,\
7... fromIndex=0,toIndex=25)
```

As an exercise, the reader is invited to try out other robust outranking based ranking heuristics. Notice also that we have not challenged in this tutorial the THE provided criteria significance preorder. It would be very interesting to consider the five ranking objectives as equally important and, consequently, consider the ranking criteria to be equisignificant. Curious to see the ranking results under such settings.

Back to Content Table

## 1.10. Computing a first choice recommendation

See also

Lecture 7 notes from the MICS Algorithmic Decision Theory course: [ADT-L7].

### 1.10.1. What site to choose ?

A SME, specialized in printing and copy services, has to move into new offices, and its CEO has gathered seven **potential office sites** (see Table 1.1).

ID |
Name |
Address |
Comment |
---|---|---|---|

A |
Ave |
Avenue de la liberté |
High standing city center |

B |
Bon |
Bonnevoie |
Industrial environment |

C |
Ces |
Cessange |
Residential suburb location |

D |
Dom |
Dommeldange |
Industrial suburb environment |

E |
Bel |
Esch-Belval |
New and ambitious urbanization far from the city |

F |
Fen |
Fentange |
Out in the countryside |

G |
Gar |
Avenue de la Gare |
Main city shopping street |

Three **decision objectives** are guiding the CEO’s choice:

minimizethe yearly costs induced by the moving,

maximizethe future turnover of the SME,

maximizethe new working conditions.

The decision consequences to take into account for evaluating the potential new office sites with respect to each of the three objectives are modelled by the following **coherent family of criteria** 26.

Objective |
ID |
Name |
Comment |
---|---|---|---|

Yearly costs |
C |
Costs |
Annual rent, charges, and cleaning |

Future turnover |
St |
Standing |
Image and presentation |

Future turnover |
V |
Visibility |
Circulation of potential customers |

Future turnover |
Pr |
Proximity |
Distance from town center |

Working conditions |
W |
Space |
Working space |

Working conditions |
Cf |
Comfort |
Quality of office equipment |

Working conditions |
P |
Parking |
Available parking facilities |

The evaluation of the seven potential sites on each criterion are gathered in the following **performance tableau**.

Criterion |
weight |
A |
B |
C |
D |
E |
F |
G |
---|---|---|---|---|---|---|---|---|

Costs |
45.0 |
35.0K€ |
17.8K€ |
6.7K€ |
14.1K€ |
34.8K€ |
18.6K€ |
12.0K€ |

Prox |
32.0 |
100 |
20 |
80 |
70 |
40 |
0 |
60 |

Visi |
26.0 |
60 |
80 |
70 |
50 |
60 |
0 |
100 |

Stan |
23.0 |
100 |
10 |
0 |
30 |
90 |
70 |
20 |

Wksp |
10.0 |
75 |
30 |
0 |
55 |
100 |
0 |
50 |

Wkcf |
6.0 |
0 |
100 |
10 |
30 |
60 |
80 |
50 |

Park |
3.0 |
90 |
30 |
100 |
90 |
70 |
0 |
80 |

Except the *Costs* criterion, all other criteria admit for grading a qualitative satisfaction scale from 0% (worst) to 100% (best). We may thus notice in Table 1.3 that site *A* is the most expensive, but also 100% satisfying the *Proximity* as well as the *Standing* criterion. Whereas the site *C* is the cheapest one; providing however no satisfaction at all on both the *Standing* and the *Working Space* criteria.

In Table 1.3 we may also see that the *Costs* criterion admits the highest significance (45.0), followed by the *Future turnover* criteria (32.0 + 26.0 + 23.0 = 81.0), The *Working conditions* criteria are the less significant (10.0 + 6.0, + 3.0 = 19.0). It follows that the CEO considers *maximizing the future turnover* the most important objective (81.0), followed by the *minizing yearly Costs* objective (45.0), and less important, the *maximizing working conditions* objective (19.0).

Concerning yearly costs, we suppose that the CEO is indifferent up to a performance difference of 1000€, and he actually prefers a site if there is at least a positive difference of 2500€. The grades observed on the six qualitative criteria (measured in percentages of satisfaction) are very subjective and rather imprecise. The CEO is hence indifferent up to a satisfaction difference of 10%, and he claims a significant preference when the satisfaction difference is at least of 20%. Furthermore, a satisfaction difference of 80% represents for him a *considerably large* performance difference, triggering a *veto* situation the case given (see [BIS-2013]).

In view of Table 1.3, what is now the office site we may recommend to the CEO as **best choice** ?

### 1.10.2. Performance tableau

A Python encoded performance tableau is available for downloading here officeChoice.py.

We may inspect the performance tableau data with the computing resources provided by the perfTabs module.

```
1>>> from perfTabs import *
2>>> t = PerformanceTableau('officeChoice')
3>>> t
4 *------- PerformanceTableau instance description ------*
5 Instance class : PerformanceTableau
6 Instance name : officeChoice
7 # Actions : 7
8 # Objectives : 3
9 # Criteria : 7
10 NaN proportion (%) : 0.0
11 Attributes : ['name', 'actions', 'objectives',
12 'criteria', 'weightPreorder',
13 'NA', 'evaluation']
14>>> t.showPerformanceTableau()
15 *---- performance tableau -----*
16 Criteria | 'C' 'Cf' 'P' 'Pr' 'St' 'V' 'W'
17 Weights | 45.00 6.00 3.00 32.00 23.00 26.00 10.00
18 ---------|---------------------------------------------------------
19 'Ave' | -35000.00 0.00 90.00 100.00 100.00 60.00 75.00
20 'Bon' | -17800.00 100.00 30.00 20.00 10.00 80.00 30.00
21 'Ces' | -6700.00 10.00 100.00 80.00 0.00 70.00 0.00
22 'Dom' | -14100.00 30.00 90.00 70.00 30.00 50.00 55.00
23 'Bel' | -34800.00 60.00 70.00 40.00 90.00 60.00 100.00
24 'Fen' | -18600.00 80.00 0.00 0.00 70.00 0.00 0.00
25 'Gar' | -12000.00 50.00 80.00 60.00 20.00 100.00 50.00
```

We thus recover all the input data. To measure the actual preference discrimination we observe on each criterion, we may use the `showCriteria()`

method.

```
1>>> t.showCriteria(IntegerWeights=True)
2 *---- criteria -----*
3 C 'Costs'
4 Scale = (Decimal('0.00'), Decimal('50000.00'))
5 Weight = 45
6 Threshold ind : 1000.00 + 0.00x ; percentile: 9.5
7 Threshold pref : 2500.00 + 0.00x ; percentile: 14.3
8 Cf 'Comfort'
9 Scale = (Decimal('0.00'), Decimal('100.00'))
10 Weight = 6
11 Threshold ind : 10.00 + 0.00x ; percentile: 9.5
12 Threshold pref : 20.00 + 0.00x ; percentile: 28.6
13 Threshold veto : 80.00 + 0.00x ; percentile: 90.5
14 ...
```

On the *Costs* criterion, 9.5% of the performance differences are considered insignificant and 14.3% below the preference discrimination threshold (lines 6-7). On the qualitative *Comfort* criterion, we observe again 9.5% of insignificant performance differences (line 11). Due to the imprecision in the subjective grading, we notice here 28.6% of performance differences below the preference discrimination threshold (Line 12). Furthermore, 100.0 - 90.5 = 9.5% of the performance differences are judged *considerably large* (Line 13); 80% and more of satisfaction differences triggering in fact a veto situation. Same information is available for all the other criteria.

A colorful comparison of all the performances is shown on Fig. 1.28 by the **heatmap** statistics, illustrating the respective quantile class of each performance. As the set of potential alternatives is tiny, we choose here a classification into performance quintiles.

```
>>> t.showHTMLPerformanceHeatmap(colorLevels=5,\
... rankingRule=None)
```

Site *Ave* shows extreme and contradictory performances: highest *Costs* and no *Working Comfort* on one hand, and total satisfaction with respect to *Standing*, *Proximity* and *Parking facilities* on the other hand. Similar, but opposite, situation is given for site *Ces*: unsatisfactory *Working Space*, no *Standing* and no *Working Comfort* on the one hand, and lowest *Costs*, best *Proximity* and *Parking facilities* on the other hand. Contrary to these contradictory alternatives, we observe two appealing compromise decision alternatives: sites *Dom* and *Gar*. Finally, site *Fen* is clearly the less satisfactory alternative of all.

### 1.10.3. Outranking digraph

To help now the CEO choosing the best site, we are going to compute pairwise outrankings (see [BIS-2013]) on the set of potential sites. For two sites *x* and *y*, the situation “*x* outranks *y*”, denoted (*x* S *y*), is given if there is:

a

significant majorityof criteria concordantly supporting that sitexisat least as satisfactory assitey, and

no considerablecounter-performance observed on any discordant criterion.

The credibility of each pairwise outranking situation (see [BIS-2013]), denoted r(*x* S *y*), is measured in a bipolar significance valuation [-1.00, 1.00], where **positive** terms r(*x* S *y*) > 0.0 indicate a **validated**, and **negative** terms r(*x* S *y*) < 0.0 indicate a **non-validated** outrankings; whereas the **median** value r(*x* S *y*) = 0.0 represents an **indeterminate** situation (see [BIS-2004a]).

For computing such a bipolar-valued outranking digraph from the given performance tableau *t*, we use the `BipolarOutrankingDigraph`

constructor from the outrankingDigraphs module. The `showHTMLRelationTable`

method shows here the resulting bipolar-valued adjacency matrix in a system browser window (see Fig. 1.29).

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> g = BipolarOutrankingDigraph(t)
3>>> g.showHTMLRelationTable()
```

In Fig. 1.29 we may notice that Alternative *D* is **positively outranking** all other potential office sites (a *Condorcet winner*). Yet, alternatives *A* (the most expensive) and *C* (the cheapest) are *not* outranked by any other site; they are in fact **weak** *Condorcet winners*.

```
1>>> g.computeCondorcetWinners()
2 ['D']
3>>> g.computeWeakCondorcetWinners()
4 ['A', 'C', 'D']
```

We may get even more insight in the apparent outranking situations when looking at the Condorcet digraph (see Fig. 1.30).

```
1>>> g.exportGraphViz('officeChoice')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to officeChoice.dot
4 dot -Grankdir=BT -Tpng officeChoice.dot -o officeChoice.png
```

One may check that the outranking digraph *g* does not admit in fact any cyclic strict preference situation.

```
1>>> g.computeChordlessCircuits()
2 []
3>>> g.showChordlessCircuits()
4 No circuits observed in this digraph.
```

### 1.10.4. *Rubis* best choice recommendations

Following the Rubis outranking method (see [BIS-2008]), potential first choice recommendations are determined by the outranking prekernels –*weakly independent* and *strictly outranking* choices– of the outranking digraph (see the tutorial on computing digraph kernels). The case given, we previously need to break open all chordless odd circuits at their weakest link.

```
1>>> from digraphs import BrokenCocsDigraph
2>>> bcg = BrokenCocsDigraph(g)
3>>> bcg.brokenLinks
4 set()
```

As we observe indeed no such chordless circuits here, we may directly compute the *prekernels* of the outranking digraph *g*.

```
1>>> g.showPreKernels()
2 *--- Computing preKernels ---*
3 Dominant preKernels :
4 ['D']
5 independence : 1.0
6 dominance : 0.02
7 absorbency : -1.0
8 covering : 1.000
9 ['B', 'E', 'C']
10 independence : 0.00
11 dominance : 0.10
12 absorbency : -1.0
13 covering : 0.500
14 ['A', 'G']
15 independence : 0.00
16 dominance : 0.78
17 absorbency : 0.00
18 covering : 0.700
19 Absorbent preKernels :
20 ['F', 'A']
21 independence : 0.00
22 dominance : 0.00
23 absorbency : 1.0
24 covering : 0.700
25 *----- statistics -----
26 graph name: rel_officeChoice.xml
27 number of solutions
28 dominant kernels : 3
29 absorbent kernels: 1
30 cardinality frequency distributions
31 cardinality : [0, 1, 2, 3, 4, 5, 6, 7]
32 dominant kernel : [0, 1, 1, 1, 0, 0, 0, 0]
33 absorbent kernel: [0, 0, 1, 0, 0, 0, 0, 0]
34 Execution time : 0.00018 sec.
35 Results in sets: dompreKernels and abspreKernels.
```

We notice in Listing 1.65 three potential first choice recommendations: the Condorcet winner *D* (Line 4), the triplet *B*, *C* and *E* (Line 9), and finally the pair *A* and *G* (Line 14). The best choice recommendation is now given by the **most determined** prekernel; the one supported by the most significant criteria coalition. This result is shown with the `showBestChoiceRecommendation()`

method. Notice that this method actually works by default on the broken chords digraph *bcg*.

```
1>>> g.showBestChoiceRecommendation(CoDual=False)
2 *****************************************
3 Rubis best choice recommendation(s) (BCR)
4 (in decreasing order of determinateness)
5 Credibility domain: [-1.00,1.00]
6 === >> potential first choice(s)
7 * choice : ['D']
8 independence : 1.00
9 dominance : 0.02
10 absorbency : -1.00
11 covering (%) : 100.00
12 determinateness (%) : 51.03
13 - most credible action(s) = { 'D': 0.02, }
14 === >> potential first choice(s)
15 * choice : ['A', 'G']
16 independence : 0.00
17 dominance : 0.78
18 absorbency : 0.00
19 covering (%) : 70.00
20 determinateness (%) : 50.00
21 - most credible action(s) = { }
22 === >> potential first choice(s)
23 * choice : ['B', 'C', 'E']
24 independence : 0.00
25 dominance : 0.10
26 absorbency : -1.00
27 covering (%) : 50.00
28 determinateness (%) : 50.00
29 - most credible action(s) = { }
30 === >> potential last choice(s)
31 * choice : ['A', 'F']
32 independence : 0.00
33 dominance : 0.00
34 absorbency : 1.00
35 covered (%) : 70.00
36 determinateness (%) : 50.00
37 - most credible action(s) = { }
38 Execution time: 0.014 seconds
```

We notice in Listing 1.66 (Line 7) above that the most significantly supported best choice recommendation is indeed the *Condorcet* winner *D* supported by a majority of 51.03% of the criteria significance (see Line 12). Both other potential first choice recommendations, as well as the potential last choice recommendation, are not positively validated as best, resp. worst choices. They may or may not be considered so. Alternative *A*, with extreme contradictory performances, appears both, in a first and a last choice recommendation (see Lines 15 and 31) and seams hence not actually comparable to its competitors.

### 1.10.5. Computing *strict best* choice recommendations

When comparing now the performances of alternatives *D* and *G* on a
pairwise perspective (see below), we notice that, with the given preference discrimination thresholds, alternative *G* is actually **certainly** *at least as good as* alternative *D*: r(*G* outranks *D*) = +145/145 = +1.0.

```
1>>> g.showPairwiseComparison('G','D')
2 *------------ pairwise comparison ----*
3 Comparing actions : (G, D)
4 crit. wght. g(x) g(y) diff. | ind pref concord |
5 =========================================================================
6 C 45.00 -12000.00 -14100.00 +2100.00 | 1000.00 2500.00 +45.00 |
7 Cf 6.00 50.00 30.00 +20.00 | 10.00 20.00 +6.00 |
8 P 3.00 80.00 90.00 -10.00 | 10.00 20.00 +3.00 |
9 Pr 32.00 60.00 70.00 -10.00 | 10.00 20.00 +32.00 |
10 St 23.00 20.00 30.00 -10.00 | 10.00 20.00 +23.00 |
11 V 26.00 100.00 50.00 +50.00 | 10.00 20.00 +26.00 |
12 W 10.00 50.00 55.00 -5.00 | 10.00 20.00 +10.00 |
13 =========================================================================
14 Valuation in range: -145.00 to +145.00; global concordance: +145.00
```

However, we must as well notice that the cheapest alternative *C* is in fact **strictly outranking** alternative *G*: r(*C* outranks *G*) = +15/145 > 0.0, and r(*G* outranks *C*) = -15/145 < 0.0.

```
1>>> g.showPairwiseComparison('C','G')
2 *------------ pairwise comparison ----*
3 Comparing actions : (C, G)/(G, C)
4 crit. wght. g(x) g(y) diff. | ind. pref. (C,G)/(G,C) |
5 ==========================================================================
6 C 45.00 -6700.00 -12000.00 +5300.00 | 1000.00 2500.00 +45.00/-45.00 |
7 Cf 6.00 10.00 50.00 -40.00 | 10.00 20.00 -6.00/ +6.00 |
8 P 3.00 100.00 80.00 +20.00 | 10.00 20.00 +3.00/ -3.00 |
9 Pr 32.00 80.00 60.00 +20.00 | 10.00 20.00 +32.00/-32.00 |
10 St 23.00 0.00 20.00 -20.00 | 10.00 20.00 -23.00/+23.00 |
11 V 26.00 70.00 100.00 -30.00 | 10.00 20.00 -26.00/+26.00 |
12 W 10.00 0.00 50.00 -50.00 | 10.00 20.00 -10.00/+10.00 |
13 =========================================================================
14 Valuation in range: -145.00 to +145.00; global concordance: +15.00/-15.00
```

To model these *strict outranking* situations, we may recompute the best choice recommendation on the **codual**, the converse (~) of the dual (-) 14, of the outranking digraph instance *g* (see [BIS-2013]), as follows.

```
1>>> g.showBestChoiceRecommendation(\
2... CoDual=True,\
3... ChoiceVector=True)
4
5 * --- First and last choice recommendation(s) ---*
6 (in decreasing order of determinateness)
7 Credibility domain: [-1.00,1.00]
8 === >> potential first choice(s)
9 * choice : ['A', 'C', 'D']
10 independence : 0.00
11 dominance : 0.10
12 absorbency : 0.00
13 covering (%) : 41.67
14 determinateness (%) : 50.59
15 - characteristic vector = { 'D': 0.02, 'G': 0.00, 'C': 0.00,
16 'A': 0.00, 'F': -0.02, 'E': -0.02,
17 'B': -0.02, }
18 === >> potential last choice(s)
19 * choice : ['A', 'F']
20 independence : 0.00
21 dominance : -0.52
22 absorbency : 1.00
23 covered (%) : 50.00
24 determinateness (%) : 50.00
25 - characteristic vector = { 'G': 0.00, 'F': 0.00, 'E': 0.00,
26 'D': 0.00, 'C': 0.00, 'B': 0.00,
27 'A': 0.00, }
```

It is interesting to notice in Listing 1.67 (Line 9) that the **strict best choice recommendation** consists in the set of weak Condorcet winners: ‘A’, ‘C’ and ‘D’. In the corresponding characteristic vector (see Line 15-17), representing the bipolar credibility degree with which each alternative may indeed be considered a best choice (see [BIS-2006a], [BIS-2006b]), we find confirmed that alternative *D* is the only positively validated one, whereas both extreme alternatives - *A* (the most expensive) and *C* (the cheapest) - stay in an indeterminate situation. They may be potential first choice candidates besides *D*. Notice furthermore that compromise alternative *G*, while not actually included in an outranking prekernel, shows as well an indeterminate situation with respect to **being or not being** a potential first choice candidate.

We may also notice (see Line 17 and Line 21) that both alternatives *A* and *F* are reported as certainly strict outranked choices, hence as **potential last choice recommendation** . This confirms again the global incomparability status of alternative *A* (see Fig. 1.31).

```
1>>> gcd = ~(-g) # codual of g
2>>> gcd.exportGraphViz(fileName='bestChoiceChoice',\
3... fistChoice=['A','C','D'],\
4... lastChoice=['F'])
5 *---- exporting a dot file for GraphViz tools ---------*
6 Exporting to bestOfficeChoice.dot
7 dot -Grankdir=BT -Tpng bestOfficeChoice.dot -o bestOfficeChoice.png
```

### 1.10.6. Weakly ordering the outranking digraph

To get a more complete insight in the overall strict outranking situations, we may use the `RankingByChoosingDigraph`

constructor imported from the transitiveDigraphs module, for computing a **ranking-by-choosing** result from the codual, i.e. the strict outranking digraph instance *gcd* (see above).

```
1>>> from transitiveDigraphs import RankingByChoosingDigraph
2>>> rbc = RankingByChoosingDigraph(gcd)
3 Threading ... ## multiprocessing if 2 cores are available
4 Exiting computing threads
5>>> rbc.showRankingByChoosing()
6 Ranking by Choosing and Rejecting
7 1st ranked ['D']
8 2nd ranked ['C', 'G']
9 2nd last ranked ['B', 'C', 'E']
10 1st last ranked ['A', 'F']
11>>> rbc.exportGraphViz('officeChoiceRanking')
12 *---- exporting a dot file for GraphViz tools ---------*
13 Exporting to officeChoiceRanking.dot
14 0 { rank = same; A; C; D; }
15 1 { rank = same; G; }
16 2 { rank = same; E; B; }
17 3 { rank = same; F; }
18 dot -Grankdir=TB -Tpng officeChoiceRanking.dot -o officeChoiceRanking.png
```

In this **ranking-by-choosing** method, where we operate the *epistemic fusion* of iterated (strict) first and last choices, compromise alternative *D* is now ranked before compromise alternative *G*. If the computing node supports multiple processor cores, first and last choosing iterations are run in parallel. The overall partial ordering result shows again the important fact that the most expensive site *A*, and the cheapest site *C*, both appear incomparable with most of the other alternatives, as is apparent from the Hasse diagram of the ranking-by-choosing relation (see Fig. 1.32).

The best choice recommendation appears hence depending on the very importance the CEO is attaching to each of the three decision objectives he is considering. In the setting here, where he considers that *maximizing the future turnover* is the most important objective followed by *minimizing the Costs* and, less important, *maximizing the working conditions*, site *D* represents actually the best compromise. However, if *Costs* do not play much a role, it would be perhaps better to decide to move to the most advantageous site *A*; or if, on the contrary, *Costs* do matter a lot, moving to the cheapest alternative *C* could definitely represent a more convincing recommendation.

It might be worth, as an **exercise**, to modify these criteria significance weights in the ‘officeChoice.py’ data file in such a way that

all criteria under an objective appear

equi-significant, andall three decision objectives are considered

equally important.

What will become the best choice recommendation under this working hypothesis?

See also

Lecture 7 notes from the MICS Algorithmic Decision Theory course: [ADT-L7].

Back to Content Table

## 1.11. Alice’s best choice: A *selection* case study 19

Alice D. , 19 years old German student finishing her secondary studies in Köln (Germany), desires to undertake foreign languages studies. She will probably receive her “Abitur” with satisfactory and/or good marks and wants to start her further studies thereafter.

She would not mind staying in Köln, yet is ready to move elsewhere if necessary. The length of the higher studies do concern her, as she wants to earn her life as soon as possible. Her parents however agree to financially support her study fees as well as her living costs during her studies.

### 1.11.1. The decision problem

Alice has already identified 10 **potential study programs**.

ID |
Diploma |
Institution |
City |
---|---|---|---|

T-UD |
Qualified translator (T) |
University (UD) |
Düsseldorf |

T-FHK |
Qualified translator (T) |
Higher Technical School (FHK) |
Köln |

T-FHM |
Qualified translator (T) |
Higher Technical School (FHM) |
München |

I-FHK |
Graduate interpreter (I) |
Higher Technical School (FHK) |
Köln |

T-USB |
Qualified translator (T) |
University (USB) |
Saarbrücken |

I-USB |
Graduate interpreter (I) |
University (USB) |
Saarbrücken |

T-UHB |
Qualified translator (T) |
University (UHB) |
Heidelberg |

I-UHB |
Graduate interpreter (I) |
University (UHB) |
Heidelberg |

S-HKK |
Specialized secretary (S) |
Chamber of Commerce (HKK) |
Köln |

C-HKK |
Foreign correspondent (C) |
Chamber of Commerce (HKK) |
Köln |

In Table 1.4 we notice that Alice considers three *Graduate Interpreter* studies (8 or 9 Semesters), respectively in Köln, in Saarbrücken or in Heidelberg; and five *Qualified translator* studies (8 or 9 Semesters), respectively in Köln, in Düsseldorf, in Saarbrücken, in Heidelberg or in Munich. She also considers two short (4 Semesters) study programs at the Chamber of Commerce in Köln.

Four **decision objectives** of more or less equal importance are guiding Alice’s choice:

maximizethe attractiveness of the study place (GEO),

maximizethe attractiveness of her further studies (LEA),

minimizeher financial dependency on her parents (FIN),

maximizeher professional perspectives (PRA).

The decision consequences Alice wishes to take into account for evaluating the potential study programs with respect to each of the four objectives are modelled by the following **coherent family of criteria** 26.

ID |
Name |
Comment |
Objective |
Weight |
---|---|---|---|---|

DH |
Proximity |
Distance in km to her home (min) |
GEO |
3 |

BC |
Big City |
Number of inhabitants (max) |
GEO |
3 |

AS |
Studies |
Attractiveness of the studies (max) |
LEA |
6 |

SF |
Fees |
Annual study fees (min) |
FIN |
2 |

LC |
Living |
Monthly living costs (min) |
FIN |
2 |

SL |
Length |
Length of the studies (min) |
FIN |
2 |

AP |
Profession |
Attractiveness of the profession (max) |
PRA |
2 |

AI |
Income |
Annual income after studying (max) |
PRA |
2 |

PR |
Prestige |
Occupational prestige (max) |
PRA |
2 |

Within each decision objective, the performance criteria are considered to be equisignificant. Hence, the four decision objectives show a same importance weight of 6 (see Table 1.5).

### 1.11.2. The performance tableau

The actual evaluations of Alice’s potential study programs are stored in a file named AliceChoice.py of `PerformanceTableau`

format 21.

```
1>>> from perfTabs import PerformanceTableau
2>>> t = PerformanceTableau('AliceChoice')
3>>> t.showObjectives()
4 *------ decision objectives -------"
5 GEO: Geographical aspect
6 DH Distance to parent's home 3
7 BC Number of inhabitants 3
8 Total weight: 6 (2 criteria)
9 LEA: Learning aspect
10 AS Attractiveness of the study program 6
11 Total weight: 6.00 (1 criteria)
12 FIN: Financial aspect
13 SF Annual registration fees 2
14 LC Monthly living costs 2
15 SL Study time 2
16 Total weight: 6.00 (3 criteria)
17 PRA: Professional aspect
18 AP Attractiveness of the profession 2
19 AI Annual professional income after studying 2
20 OP Occupational Prestige 2
21 Total weight: 6.00 (3 criteria)
```

Details of the performance criteria may be consulted in a browser view (see Fig. 1.33 below).

```
>>> t.showHTMLCriteria()
```

It is worthwhile noticing in Fig. 1.33 above that, on her subjective attractiveness scale of the study programs (criterion *AS*), Alice considers a performance differences of 7 points to be *considerable* and triggering, the case given, a *polarisation* of the outranking statement. Notice also the proportional *indifference* (1%) and *preference* (5%) discrimination thresholds shown on criterion *BC*-number of inhabitants.

In the following *heatmap view*, we may now consult Alice’s performance evaluations.

```
>>> t.showHTMLPerformanceHeatmap(\
... colorLevels=5,Correlations=True,ndigits=0)
```

Alice is subjectively evaluating the *Attractiveness* of the studies (criterion *AS*) on an ordinal scale from 0 (weak) to 10 (excellent). Similarly, she is subjectively evaluating the *Attractiveness* of the respective professions (criterion *AP*) on a three level ordinal scale from 0 (*weak*), 1 (*fair*) to 2 (*good*). Considering the *Occupational Prestige* (criterion *OP*), she looked up the SIOPS 20. All the other evaluation data she found on the internet (see Fig. 1.34).

Notice by the way that evaluations on performance criteria to be *minimized*, like *Distance to Home* (criterion *DH*) or *Study time* (criterion *SL*), are registered as *negative* values, so that smaller measures are, in this case, preferred to larger ones.

Her ten potential study programs are ordered with the *NetFlows* ranking rule applied to the corresponding bipolar-valued outranking digraph 23. *Graduate interpreter* studies in Köln (*I-FHK*) or Saarbrücken (*I-USB*), followed by *Qualified Translator* studies in Köln (*T-FHK*) appear to be Alice’s most preferred alternatives. The least attractive study programs for her appear to be studies at the Chamber of Commerce of Köln (*C-HKK*, *S-HKK*).

It is finally interesting to observe in Fig. 1.34 (third row) that the *most significant* performance criteria, appear to be for Alice, on the one side, the *Attractiveness* of the study program (criterion *AS*, tau = +0.72) followed by the *Attractiveness* of the future profession (criterion *AP*, tau = +0.62). On the other side, *Study times* (criterion *SL*, tau = -0.24), *Big city* (criterion *BC*, tau = -0.07) as well as *Monthly living costs* (criterion *LC*, tau = -0.04) appear to be for her *not so* significant 27.

### 1.11.3. Building a best choice recommendation

Let us now have a look at the resulting pairwise outranking situations.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> dg = BipolarOutrankingDigraph(t)
3>>> dg
4 *------- Object instance description ------*
5 Instance class : BipolarOutrankingDigraph
6 Instance name : rel_AliceChoice
7 # Actions : 10
8 # Criteria : 9
9 Size : 67
10 Determinateness (%) : 73.91
11 Valuation domain : [-1.00;1.00]
12>>> dg.computeSymmetryDegree(Comments=True)
13 Symmetry degree of graph <rel_AliceChoice> : 0.49
```

From Alice’s performance tableau we obtain 67 positively validated pairwise outranking situations in the digraph *dg*, supported by a 74% majority of criteria significance (see Listing 1.69 Line 9-10).

Due to the poorly discriminating performance evaluations, nearly half of these outranking situations (see Line 12) are *symmetric* and reveal actually *more or less indifference* situations between the potential study programs. This is well illustrated in the **relation map** of the outranking digraph (see Fig. 1.35).

```
>>> dg.showHTMLRelationMap(\
... tableTitle='Outranking relation map',\
... rankingRule='Copeland')
```

We have mentioned that Alice considers a performance difference of 7 points on the *Attractiveness of studies* criterion *AS* to be considerable which triggers, the case given, a potential polarisation of the outranking characteristics. In Fig. 1.35 above, these polarisations appear in the last column and last row. We may inspect the occurrence of such polarisations as follows.

```
1>>> dg.showPolarisations()
2 *---- Negative polarisations ----*
3 number of negative polarisations : 3
4 1: r(S-HKK >= I-FHK) = -0.17
5 criterion: AS
6 Considerable performance difference : -7.00
7 Veto discrimination threshold : -7.00
8 Polarisation: r(S-HKK >= I-FHK) = -0.17 ==> -1.00
9 2: r(S-HKK >= I-USB) = -0.17
10 criterion: AS
11 Considerable performance difference : -7.00
12 Veto discrimination threshold : -7.00
13 Polarisation: r(S-HKK >= I-USB) = -0.17 ==> -1.00
14 3: r(S-HKK >= I-UHB) = -0.17
15 criterion: AS
16 Considerable performance difference : -7.00
17 Veto discrimination threshold : -7.00
18 Polarisation: r(S-HKK >= I-UHB) = -0.17 ==> -1.00
19 *---- Positive polarisations ----*
20 number of positive polarisations: 3
21 1: r(I-FHK >= S-HKK) = 0.83
22 criterion: AS
23 Considerable performance difference : 7.00
24 Counter-veto threshold : 7.00
25 Polarisation: r(I-FHK >= S-HKK) = 0.83 ==> +1.00
26 2: r(I-USB >= S-HKK) = 0.17
27 criterion: AS
28 Considerable performance difference : 7.00
29 Counter-veto threshold : 7.00
30 Polarisation: r(I-USB >= S-HKK) = 0.17 ==> +1.00
31 3: r(I-UHB >= S-HKK) = 0.17
32 criterion: AS
33 Considerable performance difference : 7.00
34 Counter-veto threshold : 7.00
35 Polarisation: r(I-UHB >= S-HKK) = 0.17 ==> +1.00
```

In Listing 1.70, we see that *considerable performance differences* concerning the *Attractiveness of the studies* (*AS* criterion) are indeed observed between the *Specialised Secretary* study programm offered in Köln and the *Graduate Interpreter* study programs offered in Köln, Saarbrücken and Heidelberg. They polarise, hence, three *more or less invalid* outranking situations to *certainly invalid* (Lines 8, 13, 18) and corresponding three *more or less valid* converse outranking situations to *certainly valid* ones (Lines 25, 30, 35).

We may finally notice in the relation map, shown in Fig. 1.35, that the four best-ranked study programs, *I-FHK*, *I-USB*, *I-UHB* and *T-FHK*, are in fact *Condorcet* winners (see Listing 1.71 Line 2), i.e. they are all four *indifferent* one of the other **and** positively *outrank* all other alternatives, a result confirmed below by our best choice recommendation (Line 8).

```
1>>> dg.computeCondorcetWinners()
2 ['I-FHK', 'I-UHB', 'I-USB', 'T-FHK']
3>>> dg.showBestChoiceRecommendation()
4 Best choice recommendation(s) (BCR)
5 (in decreasing order of determinateness)
6 Credibility domain: [-1.00,1.00]
7 === >> potential first choice(s)
8 choice : ['I-FHK','I-UHB','I-USB','T-FHK']
9 independence : 0.17
10 dominance : 0.08
11 absorbency : -0.83
12 covering (%) : 62.50
13 determinateness (%) : 68.75
14 most credible action(s) = {'I-FHK': 0.75,'T-FHK': 0.17,
15 'I-USB': 0.17,'I-UHB': 0.17}
16 === >> potential last choice(s)
17 choice : ['C-HKK', 'S-HKK']
18 independence : 0.50
19 dominance : -0.83
20 absorbency : 0.17
21 covered (%) : 100.00
22 determinateness (%) : 58.33
23 most credible action(s) = {'S-HKK': 0.17,'C-HKK': 0.17}
```

Most credible best choice among the four best-ranked study programs eventually becomes the *Graduate Interpreter* study program at the *Technical High School* in *Köln* (see Listing 1.71 Line 14) supported by a (18/24) majority of global criteria significance 24.

In the relation map, shown in Fig. 1.35, we see in the left lower corner that the *asymmetric part* of the outranking relation, i.e. the corresponding *strict* outranking relation, is actually *transitive* (see Listing 1.72 Line 2). Hence, a graphviz drawing of its *skeleton*, oriented by the previous *best*, respectively *worst* choice, may well illustrate our *best choice recommendation*.

```
1>>> dgcd = ~(-dg)
2>>> dgcd.isTransitive()
3 True
4>>> dgcd.closeTransitive(Reverse=True,InSite=True)
5>>> dgcd.exportGraphViz('aliceBestChoice',\
6... bestChoice=['I-FHK'],
7... worstChoice=['S-HKK','C-HKK'])
8 *---- exporting a dot file for GraphViz tools ---------*
9 Exporting to aliceBestChoice.dot
10 dot -Grankdir=BT -Tpng aliceBestChoice.dot -o aliceBestChoice.png
```

In Fig. 1.36 we notice that the *Graduate Interpreter* studies come first, followed by the *Qualified Translator* studies. Last come the *Chamber of Commerce*’s specialised studies. This confirms again the high significance that Alice attaches to the *attractiveness* of her further studies and of her future profession (see criteria *AS* and *AP* in Fig. 1.34).

Let us now, for instance, check the pairwise outranking situations observed between the first and second-ranked alternative, i.e. *Garduate Interpreter* studies in *Köln* versus *Graduate Interpreter* studies in *Saabrücken* (see *I-FHK* and *I-USB* in Fig. 1.34).

```
>>> dg.showHTMLPairwiseOutrankings('I-FHK','I-USB')
```

The *Köln* alternative is performing **at least as well as** the *Saarbrücken* alternative on all the performance criteria, except the *Annual income* (of significance 2/24). Conversely, the *Saarbrücken* alternative is clearly **outperformed** from the *geographical* (0/6) as well as from the *financial* perspective (2/6).

In a similar way, we may finally compute a *weak ranking* of all the potential study programs with the help of the `RankingByChoosingDigraph`

constructor (see Listing 1.73 below), who computes a bipolar ranking by conjointly *best-choosing* and *last-rejecting* [BIS-1999].

```
1>>> from transitiveDigraphs import\
2... RankingByChoosingDigraph
3
4>>> rbc = RankingByChoosingDigraph(dg)
5>>> rbc.showRankingByChoosing()
6 Ranking by Choosing and Rejecting
7 1st ranked ['I-FHK']
8 2nd ranked ['I-USB']
9 3rd ranked ['I-UHB']
10 4th ranked ['T-FHK']
11 5th ranked ['T-UD']
12 5th last ranked ['T-UD']
13 4th last ranked ['T-UHB', 'T-USB']
14 3rd last ranked ['T-FHM']
15 2nd last ranked ['C-HKK']
16 1st last ranked ['S-HKK']
```

In Listing 1.73, we find confirmed that the *Interpreter* studies appear all preferrred to the *Translator* studies. Furthermore, the *Interpreter* studies in *Saarbrücken* appear preferred to the same studies in *Heidelberg*. The *Köln* alternative is apparently the preferred one of all the *Translater* studies. And, the *Foreign Correspondent* and the *Specialised Secretary* studies appear second-last and last ranked.

Yet, how *robust* are our findings with respect to potential settings of the decision objectives’ importance and the performance criteria significance ?

### 1.11.4. Robustness analysis

Alice considers her four decision objectives as being *more or less* equally important. Here we have, however, allocated *strictly equal* importance weights with *strictly* equi-significant criteria per objective. How robust is our previous best choice recommendation when, now, we would consider the importance of the objectives and, hence, the significance of the respective performance criteria to be *more or less uncertain* ?

To answer this question, we will consider the respective criteria significance weights *wj* to be **triangular random variables** in the range 0 to *2wj* with *mode* = *wj*. We may compute a corresponding **90%-confident outranking digraph** with the help of the `ConfidentBipolarOutrankingDigraph`

constructor 22.

```
1>>> from outrankingDigraphs import\
2... ConfidentBipolarOutrankingDigraph
3
4>>> cdg = ConfidentBipolarOutrankingDigraph(t,\
5... distribution='triangular',confidence=90.0)
6
7>>> cdg
8 *------- Object instance description ------*
9 Instance class : ConfidentBipolarOutrankingDigraph
10 Instance name : rel_AliceChoice_CLT
11 # Actions : 10
12 # Criteria : 9
13 Size : 44
14 Valuation domain : [-1.00;1.00]
15 Uncertainty model : triangular(a=0,b=2w)
16 Likelihood domain : [-1.0;+1.0]
17 Confidence level : 90.0%
18 Confident majority : 14/24 (58.3%)
19 Determinateness (%) : 68.19
```

Of the original 67 valid outranking situations, we retain 44 outranking situations as being 90%-*confident* (see Listing 1.74 Line 11). The corresponding 90%-*confident* **qualified majority** of criteria significance amounts to 14/24 = 58.3% (Line 15).

Concerning now a 90%-*confident* best choice recommendation, we are lucky (see Listing 1.75 below).

```
1>>> cdg.computeCondorcetWinners()
2 ['I-FHK']
3>>> cdg.showBestChoiceRecommendation()
4 ***********************
5 Best choice recommendation(s) (BCR)
6 (in decreasing order of determinateness)
7 Credibility domain: [-1.00,1.00]
8 === >> potential first choice(s)
9 choice : ['I-FHK','I-UHB','I-USB',
10 'T-FHK','T-FHM']
11 independence : 0.00
12 dominance : 0.42
13 absorbency : 0.00
14 covering (%) : 20.00
15 determinateness (%) : 61.25
16 - most credible action(s) = { 'I-FHK': 0.75, }
```

The *Graduate Interpreter* studies in Köln remain indeed a 90%-confident *Condorcet* winner (Line 2). Hence, the same study program also remains our 90%-confident most credible best choice supported by a continual 18/24 (87.5%) majority of the global criteria significance (see Lines 9-10 and 16).

When previously comparing the two best-ranked study programs (see Fig. 1.37), we have observed that *I-FHK* actually positively outranks *I-USB* on all four decision objectives. When admitting equi-significant criteria significance weights per objective, this outranking situation is hence valid independently of the importance weights Alice may allocate to each of her decision objectives.

We may compute these **unopposed** outranking situations 25 with help of the `UnOpposedBipolarOutrankingDigraph`

constructor.

```
1>>> from outrankingDigraphs import UnOpposedBipolarOutrankingDigraph
2>>> uop = UnOpposedBipolarOutrankingDigraph(t)
3>>> uop
4 *------- Object instance description ------*
5 Instance class : UnOpposedBipolarOutrankingDigraph
6 Instance name : AliceChoice_unopposed_outrankings
7 # Actions : 10
8 # Criteria : 9
9 Size : 28
10 Oppositeness (%) : 58.21
11 Determinateness (%) : 62.94
12 Valuation domain : [-1.00;1.00]
13>>> uop.isTransitive()
14 True
```

We keep 28 out the 67 standard outranking situations, which leads to an **oppositeness degree** of (1.0 - 28/67) = 58.21% (Listing 1.76 Line 10). Remarkable furthermore is that this unopposed outranking digraph *uop* is actually *transitive*, i.e. modelling a *partial ranking* of the study programs (Line 14).

We may hence make use of the `exportGraphViz()`

method of the `TransitiveDigraph`

class for drawing the corresponding partial ranking.

```
1>>> from transitiveDigraphs import TransitiveDigraph
2>>> TransitiveDigraph.exportGraphViz(uop,\
3... fileName='choice_unopposed')
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to choice_unopposed.dot
6 dot -Grankdir=TB -Tpng choice_unopposed.dot -o choice_unopposed.png
```

Again, when *equi-signficant* performance criteria are assumed per decision objective, we observe in Fig. 1.38 that *I-FHK* remains the stable best choice, *independently* of the actual importance weights that Alice may wish to allocate to her four decision objectives.

In view of her performance tableau in Fig. 1.34, *Graduate Interpreter* studies at the *Technical High School Köln*, thus, represent definitely **Alice’s very best choice**.

For further reading about the *Rubis* Best Choice methodology, one may consult in [BIS-2015] the study of a *real decision aid case* about choosing a best poster in a scientific conference.

Back to Content Table

## 1.12. Rating by sorting into relative performance quantiles

We apply order statistics for sorting a set *X* of *n* potential decision actions, evaluated on *m* incommensurable performance criteria, into *q* quantile equivalence classes, based on pairwise outranking characteristics involving the quantile class limits observed on each criterion. Thus we may implement a weak ordering algorithm of complexity .

### 1.12.1. Quantile sorting on a single criterion

A single criterion sorting category *K* is a (usually) lower-closed interval on a real-valued performance measurement scale, with . If *x* is a measured performance on this scale, we may distinguish three sorting situations.

and (): The performance

xis lower than categoryK.and : The performance

xbelongs to categoryK.and : The performance

xis higher than categoryK.

As the relation is the dual of (), it will be sufficient to check that as well as are true for *x* to be considered a member of category *K*.

Upper-closed categories (in a more mathematical integration style) may as well be considered. In this case it is sufficient to check that as well as are true for *x* to be considered a member of category *K*. It is worthwhile noticing that a category *K* such that is hence always empty by definition. In order to be able to properly sort over the complete range of values to be sorted, we will need to use a special, two-sided closed last, respectively first, category.

Let be a non trivial partition of the criterion’s performance measurement scale into ordered categories – i.e. lower-closed intervals – such that , for *k* = 0, …, *q* - 1 and . And, let be a finite set of not all equal performance measures observed on the scale in question.

**Property**: For all performance measure there exists now a unique *k* such that . If we assimilate, like in descriptive statistics, all the measures gathered in a category to the central value of the category – i.e. – the sorting result will hence define a weak order (complete preorder) on A.

Let denote the set of *q* + 1 increasing order-statistical quantiles –like quartiles or deciles– we may compute from the ordered set *A* of performance measures observed on a performance scale. If , we may, with the following intervals: , , …, , hence define a set of *q* lower-closed sorting categories. And, in the case of upper-closed categories, if , we would obtain the intervals , , …, . The corresponding sorting of *A* will result, in both cases, in a repartition of all measures *x* into the *q* quantile categories for *k* = 1, …, *q*.

**Example**: Let *A* = { , , , , , , , , , , , , , , , , , , , } be a set of 20 increasing performance measures observed on a given criterion. The lower-closed category limits we obtain with quartiles (*q* = 4) are: = , , (median performance), and . And the sorting into these four categories defines on *A* a complete preorder with the following four equivalence classes: , , , and .

### 1.12.2. Quantiles sorting with multiple performance criteria

Let us now suppose that we are given a performance tableau with a set *X* of *n* decision alternatives evaluated on a coherent family of *m* performance criteria associated with the corresponding outranking relation defined on *X*. We denote the performance of alternative *x* observed on criterion *j*.

Suppose furthermore that we want to sort the decision alternatives into *q* upper-closed quantile equivalence classes. We therefore consider a series : for *k* = 0, …, *q* of *q+1* equally spaced quantiles, like quartiles: 0, 0.25, 0.5, 0.75, 1; quintiles: 0, 0.2, 0.4, 0.6, 0.8, 1: or deciles: 0, 0.1, 0.2, …, 0.9, 1, for instance.

The upper-closed class corresponds to the *m* quantile intervals observed on each criterion *j*, where *k* = 2, …, *q* , , and the first class gathers all performances below or equal to .

The lower-closed class corresponds to the *m* quantile intervals observed on each criterion *j*, where *k* = 1, …, *q*-1, , and the last class gathers all performances above or equal to .

We call **q-tiles** a complete series of *k* = 1, …, *q* upper-closed , respectively lower-closed , multiple criteria quantile classes.

**Property**: With the help of the bipolar-valued characteristic of the outranking relation we may compute the bipolar-valued characteristic of the assertion: *x* belongs to upper-closed *q*-tiles class class, resp. lower-closed class , as follows.

The outranking relation verifying the coduality principle, , resp. .

We may compute, for instance, a five-tiling of a given random performance tableau with the help of the `QuantilesSortingDigraph`

class.

```
1>>> from randomPerfTabs import *
2>>> t = RandomPerformanceTableau(numberOfActions=50,seed=5)
3>>> from sortingDigraphs import QuantilesSortingDigraph
4>>> qs = QuantilesSortingDigraph(t,limitingQuantiles=5)
5>>> qs
6 *----- Object instance description -----------*
7 Instance class : QuantilesSortingDigraph
8 Instance name : sorting_with_5-tile_limits
9 # Actions : 50
10 # Criteria : 7
11 # Categories : 5
12 Lowerclosed : False
13 Size : 841
14 Valuation domain : [-1.00;1.00]
15 Determinateness (%) : 81.39
16 Attributes : ['actions', 'actionsOrig',
17 'criteria', 'evaluation', 'runTimes', 'name',
18 'limitingQuantiles', 'LowerClosed',
19 'categories', 'criteriaCategoryLimits',
20 'profiles', 'profileLimits', 'hasNoVeto',
21 'valuationdomain', 'nbrThreads', 'relation',
22 'categoryContent', 'order', 'gamma', 'notGamma']
23 *------ Constructor run times (in sec.) ------*
24 # Threads : 1
25 Total time : 0.03120
26 Data input : 0.00300
27 Compute profiles : 0.00075
28 Compute relation : 0.02581
29 Weak Ordering : 0.00052
30>>> qs.showCriteriaQuantileLimits()
31 Quantile Class Limits (q = 5)
32 Upper-closed classes
33 crit. 0.20 0.40 0.60 0.80 1.00
34 *------------------------------------------------
35 g1 31.35 41.09 58.53 71.91 98.08
36 g2 27.81 39.19 49.87 61.66 96.18
37 g3 25.10 34.78 49.45 63.97 92.59
38 g4 24.61 37.91 53.91 71.02 89.84
39 g5 26.94 36.43 52.16 72.52 96.25
40 g6 23.94 44.06 54.92 67.34 95.97
41 g7 30.94 47.40 55.46 69.04 97.10
42>>> qs.showSorting()
43 *--- Sorting results in descending order ---*
44 ]0.80 - 1.00]: ['a22']
45 ]0.60 - 0.80]: ['a03', 'a07', 'a08', 'a11', 'a14', 'a17',
46 'a19', 'a20', 'a29', 'a32', 'a33', 'a37',
47 'a39', 'a41', 'a42', 'a49']
48 ]0.40 - 0.60]: ['a01', 'a02', 'a04', 'a05', 'a06', 'a08',
49 'a09', 'a16', 'a17', 'a18', 'a19', 'a21',
50 'a24', 'a27', 'a28', 'a30', 'a31', 'a35',
51 'a36', 'a40', 'a43', 'a46', 'a47', 'a48',
52 'a49', 'a50']
53 ]0.20 - 0.40]: ['a04', 'a10', 'a12', 'a13', 'a15', 'a23',
54 'a25', 'a26', 'a34', 'a38', 'a43', 'a44',
55 'a45', 'a49']
56 ] < - 0.20]: ['a44']
```

Most of the decision actions (26) are gathered in the median quintile class, whereas the highest quintile and the lowest quintile classes gather each one a unique decision alternative (*a22*, resp. *a44*) (see Listing 1.77 Lines 43-).

We may inspect as follows the details of the corresponding sorting characteristics.

```
1>>> qs.valuationdomain
2 {'min': Decimal('-1.0'), 'med': Decimal('0'),
3 'max': Decimal('1.0')}
4>>> qs.showSortingCharacteristics()
5 x in q^k r(q^k-1 < x) r(q^k >= x) r(x in q^k)
6 a22 in ]< - 0.20] 1.00 -0.86 -0.86
7 a22 in ]0.20 - 0.40] 0.86 -0.71 -0.71
8 a22 in ]0.40 - 0.60] 0.71 -0.71 -0.71
9 a22 in ]0.60 - 0.80] 0.71 -0.14 -0.14
10 a22 in ]0.80 - 1.00] 0.14 1.00 0.14
11 ...
12 ...
13 a44 in ]< - 0.20] 1.00 0.00 0.00
14 a44 in ]0.20 - 0.40] 0.00 0.57 0.00
15 a44 in ]0.40 - 0.60] -0.57 0.86 -0.57
16 a44 in ]0.60 - 0.80] -0.86 0.86 -0.86
17 a44 in ]0.80 - 1.00] -0.86 0.86 -0.86
18 ...
19 ...
20 a49 in ]< - 0.20] 1.00 -0.43 -0.43
21 a49 in ]0.20 - 0.40] 0.43 0.00 0.00
22 a49 in ]0.40 - 0.60] 0.00 0.00 0.00
23 a49 in ]0.60 - 0.80] 0.00 0.57 0.00
24 a49 in ]0.80 - 1.00] -0.57 0.86 -0.57
```

Alternative *a22* verifies indeed positively both sorting conditions only for the highest quintile class (see Listing 1.78 Lines 10). Whereas alternatives *a44* and *a49*, for instance, weakly verify both sorting conditions each one for two, resp. three, adjacent quintile classes (see Lines 13-14 and 21-23).

Quantiles sorting results indeed always verify the following **Properties**.

Coherence: Each object is sorted into a non-empty subset ofadjacentq-tiles classes. An alternative that wouldmissevaluations on all the criteria will be sorted conjointly in all q-tiled classes.

Uniqueness: If fork= 1, …,q, then performancexis sorted intoexactly one singleq-tiled class.

Separability: Computing the sorting result for performancexis independent from the computing of the other performances’ sorting results. This property gives access to efficient parallel processing of class membership characteristics.

The *q-tiles* sorting result leaves us hence with more or less *overlapping* ordered quantile equivalence classes. For constructing now a linearly ranked q-tiles partition of *X* , we may apply three strategies:

Average(default): In decreasing lexicographic order of the average of the lower and upper quantile limits and the upper quantile class limit;

Optimistic: In decreasing lexicographic order of the upper and lower quantile class limits;

Pessimistic: In decreasing lexicographic order of the lower and upper quantile class limits;

```
1>>> qs.showQuantileOrdering(strategy='average')
2 ]0.80-1.00] : ['a22']
3 ]0.60-0.80] : ['a03', 'a07', 'a11', 'a14', 'a20', 'a29',
4 'a32', 'a33', 'a37', 'a39', 'a41', 'a42']
5 ]0.40-0.80] : ['a08', 'a17', 'a19']
6 ]0.20-0.80] : ['a49']
7 ]0.40-0.60] : ['a01', 'a02', 'a05', 'a06', 'a09', 'a16',
8 'a18', 'a21', 'a24', 'a27', 'a28', 'a30',
9 'a31', 'a35', 'a36', 'a40', 'a46', 'a47',
10 'a48', 'a50']
11 ]0.20-0.60] : ['a04', 'a43']
12 ]0.20-0.40] : ['a10', 'a12', 'a13', 'a15', 'a23', 'a25',
13 'a26', 'a34', 'a38', 'a45']
14 ] < -0.40] : ['a44']
```

Following, for instance, the *average* ranking strategy, we find confirmed in the weak ranking shown in Listing 1.79, that alternative *a49* is indeed sorted into three adjacent quintiles classes, namely (see Line 6) and precedes the class, of same average of lower and upper limits.

The `QuantilesSortingDigraph`

constructor gives hence a linearly ordered decomposition of the corresponding bipolar-valued outranking digraph. This decomposition leads us to a new **sparse pre-ranked** outranking digraph model.

### 1.12.3. The sparse pre-ranked outranking digraph model

We may notice that a given outranking digraph -the association of a set of decision alternatives and an outranking relation- is, following the methodological requirements of the outranking approach, necessarily associated with a corresponding performance tableau. And, we may use this underlying performance tableau for linearly decomposing the set of potential decision alternatives into **ordered quantiles equivalence classes** by using the quantiles sorting technique seen in the previous Section.

In the coding example shown in Listing 1.80 below, we generate for instance, first (Lines 2-3), a simple performance tableau of 75 decision alternatives and, secondly (Lines 4), we construct the corresponding `PreRankedOutrankingDigraph`

instance called *prg*. Notice by the way the *BigData* flag (Line 3) used here for generating a parsimoniously commented performance tableau.

```
1>>> from sparseOutrankingDigraphs import *
2>>> tp = RandomPerformanceTableau(numberOfActions=75,\
3... BigData=True,seed=100)
4
5>>> prg = PreRankedOutrankingDigraph(tp,quantiles=5)
6>>> prg
7 *----- Object instance description ------*
8 Instance class : PreRankedOutrankingDigraph
9 Instance name : randomperftab_pr
10 # Actions : 75
11 # Criteria : 7
12 Sorting by : 5-Tiling
13 Ordering strategy : average
14 # Components : 9
15 Minimal order : 1
16 Maximal order : 25
17 Average order : 8.3
18 fill rate : 20.432%
19 Attributes : ['actions', 'criteria', 'evaluation', 'NA', 'name',
20 'order', 'runTimes', 'dimension', 'sortingParameters',
21 'valuationdomain', 'profiles', 'categories', 'sorting',
22 'decomposition', 'nbrComponents', 'components',
23 'fillRate', 'minimalComponentSize', 'maximalComponentSize', ... ]
```

The ordering of the 5-tiling result is following the **average** lower and upper quintile limits strategy (see previous section and Listing 1.80 Line 12). We obtain here 9 ordered components of minimal order 1 and maximal order 25. The corresponding **pre-ranked decomposition** may be visualized as follows.

```
1>>> prg.showDecomposition()
2 *--- quantiles decomposition in decreasing order---*
3 c1. ]0.80-1.00] : [5, 42, 43, 47]
4 c2. ]0.60-1.00] : [73]
5 c3. ]0.60-0.80] : [1, 4, 13, 14, 22, 32, 34, 35, 40,
6 41, 45, 61, 62, 65, 68, 70, 75]
7 c4. ]0.40-0.80] : [2, 54]
8 c5. ]0.40-0.60] : [3, 6, 7, 10, 15, 18, 19, 21, 23, 24,
9 27, 30, 36, 37, 48, 51, 52, 56, 58,
10 63, 67, 69, 71, 72, 74]
11 c6. ]0.20-0.60] : [8, 11, 25, 28, 64, 66]
12 c7. ]0.20-0.40] : [12, 16, 17, 20, 26, 31, 33, 38, 39,
13 44, 46, 49, 50, 53, 55]
14 c8. ] <-0.40] : [9, 29, 60]
15 c9. ] <-0.20] : [57, 59]
```

The highest quintile class (]80%-100%]) contains decision alternatives *5*, *42*, *43* and *47*. Lowest quintile class (]-20%]) gathers alternatives *57* and *59* (see Listing 1.81 Lines 3 and 15). We may inspect the resulting sparse outranking relation map as follows in a browser view.

```
>>> prg.showHTMLRelationMap()
```

In Fig. 1.39 we easily recognize the 9 linearly ordered quantile equivalence classes. *Green* and *light-green* show positive **outranking** situations, whereas positive **outranked** situations are shown in **red** and **light-red**. Indeterminate situations appear in white. In each one of the 9 quantile equivalence classes we recover in fact the corresponding bipolar-valued outranking *sub-relation*, which leads to an actual **fill-rate** of 20.4% (see Listing 1.80 Line 20).

We may now check how faithful the sparse model represents the complete outranking relation.

```
1>>> g = BipolarOutrankingDigraph(tp)
2>>> corr = prg.computeOrdinalCorrelation(g)
3>>> g.showCorrelation(corr)
4 Correlation indexes:
5 Crisp ordinal correlation : +0.863
6 Epistemic determination : 0.315
7 Bipolar-valued equivalence : +0.272
```

The ordinal correlation index between the standard and the sparse outranking relations is quite high (+0.863) and their bipolar-valued equivalence is supported by a mean criteria significance majority of (1.0+0.272)/2 = 64%.

It is worthwhile noticing in Listing 1.80 Line 18 that sparse pre-ranked outranking digraphs do not contain a *relation* attribute. The access to pairwise outranking characteristic values is here provided via a corresponding `relation()`

function.

```
1def relation(self,x,y):
2 """
3 Dynamic construction of the global
4 outranking characteristic function r(x,y).
5 """
6 Min = self.valuationdomain['min']
7 Med = self.valuationdomain['med']
8 Max = self.valuationdomain['max']
9 if x == y:
10 return Med
11 cx = self.actions[x]['component']
12 cy = self.actions[y]['component']
13 if cx == cy:
14 return self.components[cx]['subGraph'].relation[x][y]
15 elif self.components[cx]['rank'] > self.components[cy]['rank']:
16 return Min
17 else:
18 return Max
```

All reflexive situations are set to the *indeterminate* value. When two decision alternatives belong to a same component -quantile equivalence class- we access the relation attribute of the corresponding outranking sub-digraph. Otherwise we just check the respective ranks of the components.

### 1.12.4. Ranking pre-ranked sparse outranking digraphs

Each one of these 9 ordered components may now be locally ranked by using a suitable ranking rule. Best operational results, both in run times and quality, are more or less equally given with the *Copeland* and the *NetFlows* rules. The eventually obtained linear ordering (from the worst to best) is stored in a *prg.boostedOrder* attribute. A reversed linear ranking (from the best to the worst) is stored in a *prg.boostedRanking* attribute.

```
1>>> prg.boostedRanking
2 [43, 47, 42, 5, 73, 65, 68, 32, 62, 70, 35, 22, 75, 45, 1,
3 61, 41, 34, 4, 13, 40, 14, 2, 54, 63, 37, 56, 71, 69, 36,
4 19, 72, 15, 48, 6, 30, 74, 3, 21, 58, 52, 18, 7, 24, 27,
5 23, 67, 51, 10, 25, 11, 8, 64, 28, 66, 53, 12, 31, 39, 55,
6 20, 46, 49, 16, 44, 26, 38, 33, 17, 50, 29, 60, 9, 59, 57]
```

Alternative *43* appears *first ranked*, whereas alternative *57* is *last ranked* (see Listing 1.82 Line 2 and 6). The quality of this ranking result may be assessed by computing its ordinal correlation with the standard outranking relation.

```
1>>> corr = g.computeRankingCorrelation(prg.boostedRanking)
2>>> g.showCorrelation(corr)
3 Correlation indexes:
4 Crisp ordinal correlation : +0.807
5 Epistemic determination : 0.315
6 Bipolar-valued equivalence : +0.254
```

We may also verify below that the *Copeland* ranking obtained from the standard outranking digraph is highly correlated (+0.822) with the one obtained from the sparse outranking digraph.

```
1>>> from linearOrders import CopelandOrder
2>>> cop = CopelandOrder(g)
3>>> print(cop.computeRankingCorrelation(prg.boostedRanking))
4 {'correlation': 0.822, 'determination': 1.0}
```

Noticing the computational efficiency of the quantiles sorting construction, coupled with the separability property of the quantile class membership characteristics computation, we will make usage of the `PreRankedOutrankingDigraph`

constructor in the cythonized Digraph3 modules for HPC ranking big and even huge performance tableaux.

Back to Content Table

## 1.13. Rating by ranking with learned performance quantile norms

### 1.13.1. Introduction

In this tutorial we address the problem of **rating multiple criteria performances** of a set of potential decision alternatives with respect to empirical order statistics, i.e. performance quantiles learned from historical performance data gathered from similar decision alternatives observed in the past (see [CPSTAT-L5]).

To illustrate the decision problem we face, consider for a moment that, in a given decision aid study, we observe, for instance in the Table below, the multi-criteria performances of two potential decision alternatives, named *a1001* and *a1010*, marked on 7 **incommensurable** preference criteria: 2 **costs** criteria *c1* and *c2* (to **minimize**) and 5 **benefits** criteria *b1* to *b5* (to **maximize**).

Criterion

b1

b2

b3

b4

b5

c1

c2

weight

2

2

2

2

2

5

5

a100137.0

2

2

61.0

31.0

-4

-40.0

a101032.0

9

6

55.0

51.0

-4

-35.0

The performances on *benefits* criteria *b1*, *b4* and *b5* are measured on a cardinal scale from 0.0 (worst) to 100.0 (best) whereas, the performances on the *benefits* criteria *b2* and *b3* and on the *cost* criterion *c1* are measured on an ordinal scale from 0 (worst) to 10 (best), respectively -10 (worst) to 0 (best). The performances on the *cost* criterion *c2* are again measured on a cardinal negative scale from -100.00 (worst) to 0.0 (best).

The importance (sum of weights) of the *costs* criteria is **equal** to the importance (sum of weights) of the *benefits* criteria taken all together.

The non trivial decision problem we now face here, is to decide, how the multiple criteria performances of *a1001*, respectively *a1010*, may be rated (**excellent** ? **good** ?, or **fair** ?; perhaps even, **weak** ? or **very weak** ?) in an **order statistical sense**, when compared with all potential similar multi-criteria performances one has already encountered in the past.

To solve this *absolute* rating decision problem, first, we need to estimate multi-criteria **performance quantiles** from historical records.

### 1.13.2. Incremental learning of historical performance quantiles

See also

The technical documentation of the performanceQuantiles module.

Suppose that we see flying in random multiple criteria performances from a given model of random performance tableau (see the `randomPerfTabs`

module). The question we address here is to estimate empirical performance quantiles on the basis of so far observed performance vectors. For this task, we are inspired by [CHAM-2006] and [NR3-2007], who present an efficient algorithm for incrementally updating a quantile-binned cumulative distribution function (CDF) with newly observed CDFs.

The `PerformanceQuantiles`

class implements such a performance quantiles estimation based on a given performance tableau. Its main components are:

Ordered

objectivesand acriteriadictionaries from a valid performance tableau instance;A list

quantileFrequenciesof quantile frequencies likequartiles[0.0, 0.25, 05, 0.75,1.0],quintiles[0.0, 0.2, 0.4, 0.6, 0.8, 1.0] ordeciles[0.0, 0.1, 0.2, … 1.0] for instance;An ordered dictionary

limitingQuantilesof so far estimatedlower(default) orupperquantile class limits for each frequency per criterion;An ordered dictionary

historySizesfor keeping track of the number of evaluations seen so far per criterion. Missing data may make these sizes vary from criterion to criterion.

Below, an example Python session concerning 900 decision alternatives randomly generated from a *Cost-Benefit* Performance tableau model from which are also drawn the performances of alternatives *a1001* and *a1010* above.

```
1>>> from performanceQuantiles import PerformanceQuantiles
2>>> from randomPerfTabs import RandomCBPerformanceTableau
3>>> nbrActions=900
4>>> nbrCrit = 7
5>>> seed = 100
6>>> tp = RandomCBPerformanceTableau(numberOfActions=nbrActions,\
7... numberOfCriteria=nbrCrit,seed=seed)
8
9>>> pq = PerformanceQuantiles(tp,\
10... numberOfBins = 'quartiles',\
11... LowerClosed=True)
12
13>>> pq
14 *------- PerformanceQuantiles instance description ------*
15 Instance class : PerformanceQuantiles
16 Instance name : 4-tiled_performances
17 # Objectives : 2
18 # Criteria : 7
19 # Quantiles : 4
20 # History sizes : {'c1': 887, 'b1': 888, 'b2': 891, 'b3': 895,
21 'b4': 892, 'c2': 893, 'b5': 887}
22 Attributes : ['perfTabType', 'valueDigits', 'actionsTypeStatistics',
23 'objectives', 'BigData', 'missingDataProbability',
24 'criteria', 'LowerClosed', 'name',
25 'quantilesFrequencies', 'historySizes',
26 'limitingQuantiles', 'cdf']
```

The `PerformanceQuantiles`

class parameter *numberOfBins* (see Listing 1.83 Line 10 above), choosing the wished number of quantile frequencies, may be either **quartiles** (4 bins), **quintiles** (5 bins), **deciles** (10 bins), **dodeciles** (20 bins) or any other integer number of quantile bins. The quantile bins may be either **lower closed** (default) or **upper-closed**.

```
1>>> pq.showLimitingQuantiles(ByObjectives=True)
2 ---- Historical performance quantiles -----*
3 Costs
4 criteria | weights | '0.00' '0.25' '0.50' '0.75' '1.00'
5 ---------|-------------------------------------------------------
6 'c1' | 5 | -10 -7 -5 -3 0
7 'c2' | 5 | -96.37 -70.65 -50.10 -30.00 -1.43
8 Benefits
9 criteria | weights | '0.00' '0.25' '0.50' '0.75' '1.00'
10 ---------|-------------------------------------------------------
11 'b1' | 2 | 1.99 29.82 49,44 70.73 99.83
12 'b2' | 2 | 0 3 5 7 10
13 'b3' | 2 | 0 3 5 7 10
14 'b4' | 2 | 3.27 30.10 50.82 70.89 98.05
15 'b5' | 2 | 0.85 29.08 48.55 69.98 97.56
```

Both objectives are **equi-important**; the sum of weights (10) of the *costs* criteria balance the sum of weights (10) of the *benefits* criteria (see Listing 1.84 column 2). The preference direction of the *costs* criteria *c1* and *c2* is **negative**; the lesser the costs the better it is, whereas all the *benefits* criteria *b1* to *b5* show **positive** preference directions, i.e. the higher the benefits the better it is. The columns entitled ‘0.00’, resp. ‘1.00’ show the *quartile* *Q0*, resp. *Q4*, i.e. the **worst**, resp. **best** performance observed so far on each criterion. Column ‘0.50’ shows the **median** (*Q2*) performance observed on the criteria.

New decision alternatives with random multiple criteria performance vectors from the same random performance tableau model may now be generated with ad hoc random performance generators. We provide for experimental purpose, in the `randomPerfTabs`

module, three such generators: one for the standard `RandomPerformanceTableau`

model, one the for the two objectives `RandomCBPerformanceTableau`

Cost-Benefit model, and one for the `Random3ObjectivesPerformanceTableau`

model with three objectives concerning respectively economic, environmental or social aspects.

Given a new Performance Tableau with 100 new decision alternatives, the so far estimated historical quantile limits may be updated as follows:

```
1>>> from randomPerfTabs import RandomPerformanceGenerator
2>>> rpg = RandomPerformanceGenerator(tp,seed=seed)
3>>> newTab = rpg.randomPerformanceTableau(100)
4>>> # Updating the quartile norms shown above
5>>> pq.updateQuantiles(newTab,historySize=None)
```

Parameter *historySize* (see Listing 1.85 Line 5) of the `updateQuantiles()`

method allows to **balance** the **new** evaluations against the **historical** ones. With **historySize = None** (the default setting), the balance in the example above is 900/1000 (90%, weight of historical data) against 100/1000 (10%, weight of the new incoming observations). Putting **historySize = 0**, for instance, will ignore all historical data (0/100 against 100/100) and restart building the quantile estimation with solely the new incoming data. The updated quantile limits may be shown in a browser view (see Fig. 1.40).

```
1>>> # showing the updated quantile limits in a browser view
2>>> pq.showHTMLLimitingQuantiles(Transposed=True)
```

### 1.13.3. Rating-by-ranking new performances with quantile norms

For **absolute** *rating* of a newly given set of decision alternatives with the help of empirical performance quantiles estimated from historical data, we provide the `LearnedQuantilesRatingDigraph`

class, a specialisation of the `SortingDigraph`

class. The rating result is computed by **ranking** the new performance records together with the learned quantile limits. The constructor requires a valid `PerformanceQuantiles`

instance. By default, the constructor uses, by default, *Copeland*’s or the *NetFlows* ranking rule which **best fits** with the underlying outranking digraph.

Note

It is important to notice that the `LearnedQuantilesRatingDigraph`

class, contrary to the generic `OutrankingDigraph`

class, does not inherit from the generic `PerformanceTableau`

class, but instead from the `PerformanceQuantiles`

class. The **actions** in such a `LearnedQuantilesRatingDigraph`

class instance contain not only the newly given decision alternatives, but also the historical quantile profiles obtained from a given `PerformanceQuantiles`

class instance, i.e. estimated quantile bins’ performance limits from historical performance data.

We reconsider the `PerformanceQuantiles`

object instance *pq* as computed in the previous section. Let *newActions* be a list of 10 new decision alternatives generated with the same random performance tableau model and including the two decision alternatives *a1001* and *a1010* mentioned at the beginning.

```
1>>> from sortingDigraphs import LearnedQuantilesRatingDigraph
2>>> newActions = rpg.randomActions(10)
3>>> lqr = LearnedQuantilesRatingDigraph(pq,newActions,rankingRule='best')
4>>> lqr
5 *---- Object instance description
6 Instance class : LearnedQuantilesRatingDigraph
7 Instance name : normedRatingDigraph
8 # Criteria : 7
9 # Quantile profiles : 4
10 Lower-closed bins : True
11 # New actions : 10
12 Size : 93
13 Determinateness (%) : 76.1
14 Ranking rule : Copeland
15 Ordinal correlation : +0.95
16 Attributes: ['runTimes','objectives','criteria',
17 'LowerClosed','quantilesFrequencies','limitingQuantiles',
18 'historySizes','cdf','name','newActions','evaluation',
19 'categories','criteriaCategoryLimits','profiles','profileLimits',
20 'hasNoVeto','actions','completeRelation','relation',
21 'concordanceRelation','valuationdomain','order','gamma',
22 'notGamma','rankingRule','rankingCorrelation','rankingScores',
23 'actionsRanking','ratingCategories','ratingRelation','relationOrig']
24 *---- Constructor run times (in sec.)
25 #Threads : 1
26 Total time : 0.02218
27 Data input : 0.00134
28 Quantile classes : 0.00008
29 Compute profiles : 0.00021
30 Compute relation : 0.01869
31 Compute rating : 0.00186
32 Compute sorting : 0.00000
```

Data input to the `LearnedQuantilesRatingDigraph`

class constructor (see Listing 1.86 Line 3) are a valid PerformanceQuantiles object *pq* and a compatible list *newActions* of new decision alternatives generated from the same random origin.

Let us have a look at the digraph’s nodes, here called **newActions**.

```
1>>> lqr.showPerformanceTableau(actionsSubset=lqr.newActions)
2 *---- performance tableau -----*
3 criteria | a1001 a1002 a1003 a1004 a1005 a1006 a1007 a1008 a1009 a1010
4 ---------|-------------------------------------------------------------
5 'b1' | 37.0 27.0 24.0 16.0 42.0 33.0 39.0 64.0 42.0 32.0
6 'b2' | 2.0 5.0 8.0 3.0 3.0 3.0 6.0 5.0 4.0 9.0
7 'b3' | 2.0 4.0 2.0 1.0 6.0 3.0 2.0 6.0 6.0 6.0
8 'b4' | 61.0 54.0 74.0 25.0 28.0 20.0 20.0 49.0 44.0 55.0
9 'b5' | 31.0 63.0 61.0 48.0 30.0 39.0 16.0 96.0 57.0 51.0
10 'c1' | -4.0 -6.0 -8.0 -5.0 -1.0 -5.0 -1.0 -6.0 -6.0 -4.0
11 'c2' | -40.0 -23.0 -37.0 -37.0 -24.0 -27.0 -73.0 -43.0 -94.0 -35.0
```

Among the 10 new incoming decision alternatives (see Listing 1.87), we recognize alternatives *a1001* (see column 2) and *a1010* (see last column) we have mentioned in our introduction.

The `LearnedQuantilesRatingDigraph`

class instance’s *actions* dictionary includes as well the closed lower limits of the four quartile classes: *m1* = [0.0- [, *m2* = [0.25- [, *m3* = [0.5- [, *m4* = [0.75 - [. We find these limits in a *profiles* attribute (see Listing 1.88 below).

```
1>>> lqr.showPerformanceTableau(actionsSubset=lqr.profiles)
2 *---- Quartiles limit profiles -----*
3 criteria | 'm1' 'm2' 'm3' 'm4'
4 ---------|----------------------------
5 'b1' | 2.0 28.8 49.6 75.3
6 'b2' | 0.0 2.9 4.9 6.7
7 'b3' | 0.0 2.9 4.9 8.0
8 'b4' | 3.3 35.9 58.6 72.0
9 'b5' | 0.8 32.8 48.1 69.7
10 'c1' | -10.0 -7.4 -5.4 -3.4
11 'c2' | -96.4 -72.2 -52.3 -34.0
```

The main run time (see Listing 1.86 Lines 23-29) is spent by the class constructor in computing a bipolar-valued outranking relation on the extended actions set including both the new alternatives as well as the quartile class limits. In case of large volumes, i.e. many new decision alternatives and centile classes for instance, a multi-threading version may be used when multiple processing cores are available (see the technical description of the `LearnedQuantilesRatingDigraph`

class).

The actual rating procedure will rely on a complete ranking of the new decision alternatives as well as the quantile class limits obtained from the corresponding bipolar-valued outranking digraph. Two efficient and scalable ranking rules, the **Copeland** and its valued version, the **Netflows** rule may be used for this purpose. The *rankingRule* parameter allows to choose one of both. With *rankingRule=’best’* (see Listing 1.88 Line 2 ) the `LearnedQuantilesRatingDigraph`

constructor will choose the ranking rule that results in the highest ordinal correlation with the given outranking relation (see [BIS-2012]).

In this rating example, the *Copeland* rule appears to be the more appropriate ranking rule.

```
1>>> lqr.rankingRule
2 'Copeland'
3>>> lqr.actionsRanking
4 ['m4', 'a1005', 'a1010', 'a1002', 'a1008', 'a1006', 'a1001',
5 'a1003', 'm3', 'a1007', 'a1004', 'a1009', 'm2', 'm1']
6>>> lqr.showCorrelation(lqr.rankingCorrelation)
7 Correlation indexes:
8 Crisp ordinal correlation : +0.945
9 Epistemic determination : 0.522
10 Bipolar-valued equivalence : +0.493
```

We achieve here (see Listing 1.89) a linear ranking without ties (from best to worst) of the digraph’s actions set, i.e. including the new decision alternatives as well as the quartile limits *m1* to *m4*, which is very close in an ordinal sense to the underlying strict outranking relation.

The eventual rating procedure is based in this example on the *lower* quartile limits, such that we may collect the quartile classes’ contents in increasing order of the *quartiles*.

```
1>>> lqr.ratingCategories
2 OrderedDict([
3 ('m2', ['a1007','a1004','a1009']),
4 ('m3', ['a1005','a1010','a1002','a1008','a1006','a1001','a1003'])
5 ])
```

We notice above that no new decision alternatives are actually rated in the lowest [0.0-0.25[, respectively highest [0.75- [ quartile classes. Indeed, the rating result is shown, in descending order, as follows:

```
1>>> lqr.showQuantilesRating()
2 *-------- Quartiles rating result ---------
3 [0.50 - 0.75[ ['a1005', 'a1010', 'a1002', 'a1008',
4 'a1006', 'a1001', 'a1003']
5 [0.25 - 0.50[ ['a1007', 'a1004', 'a1009']
```

The same result may more conveniently be consulted in a browser view via a specialised rating heatmap format ( see `showHTMLPerformanceHeatmap()`

method (see Fig. 1.41).

```
1>>> lqr.showHTMLRatingHeatmap(\
2... pageTitle='Heatmap of Quartiles Rating',\
3... Correlations=True,colorLevels=5)
```

Using furthermore a specialised version of the `exportGraphViz()`

method allows drawing the same rating result in a Hasse diagram format (see Fig. 1.42).

```
1>>> lqr.exportRatingGraphViz('normedRatingDigraph')
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to normedRatingDigraph.dot
4 dot -Grankdir=TB -Tpng normedRatingDigraph.dot -o normedRatingDigraph.png
```

We may now answer the **absolute rating decision problem** stated at the beginning. Decision alternative *a1001* and alternative *a1010* (see below) are both rated into the same quartile **Q3** class (see Fig. 1.42), even if the *Copeland* ranking, obtained from the underlying strict outranking digraph (see Fig. 1.41), suggests that alternative *a1010* is effectively *better performing than* alternative *a1001*.

Criterion

b1

b2

b3

b4

b5

c1

c2

weight

2

2

2

2

2

5

5

a100137.0

2

2

61.0

31.0

-4

-40.0

a101032.0

9

6

55.0

51.0

-4

-35.0

A preciser rating result may indeed be achieved when using **deciles** instead of *quartiles* for estimating the historical marginal cumulative distribution functions.

```
1>>> pq1 = PerformanceQuantiles(tp, numberOfBins = 'deciles',\
2... LowerClosed=True)
3
4>>> pq1.updateQuantiles(newTab,historySize=None)
5>>> lqr1 = LearnedQuantilesRatingDigraph(pq1,newActions,rankingRule='best')
6>>> lqr1.showQuantilesRating()
7 *-------- Deciles rating result ---------
8 [0.60 - 0.70[ ['a1005', 'a1010', 'a1008', 'a1002']
9 [0.50 - 0.60[ ['a1006', 'a1001', 'a1003']
10 [0.40 - 0.50[ ['a1007', 'a1004']
11 [0.30 - 0.40[ ['a1009']
```

Compared with the quartiles rating result, we notice in Listing 1.91 that the seven alternatives (*a1001*, *a1002*, *a1003*, *a1005*, *a1006*, *a1008* and *a1010*), rated before into the third quartile class [0.50-0.75[, are now divided up: alternatives *a1002*, *a1005*, *a1008* and *a1010* attain now the 7th decile class [0.60-0.70[, whereas alternatives *a1001*, *a1003* and *a1006* attain only the 6th decile class [0.50-0.60[. Of the three *Q2* [0.25-0.50[ rated alternatives (*a1004*, *a1007* and *a1009*), alternatives *a1004* and *a1007* are now rated into the 5th decile class [0.40-0.50[ and *a1009* is lowest rated into the 4th decile class [0.30-0.40[.

A browser view may again more conveniently illustrate this refined rating result (see Fig. 1.43).

```
1>>> lqr1.showHTMLRatingHeatmap(\
2... pageTitle='Heatmap of the deciles rating',\
3... colorLevels=5, Correlations=True)
```

In this *deciles* rating, decision alternatives *a1001* and *a1010* are now, as expected, rated in the *6th* decile (D6), respectively in the *7th* decile (D7).

To avoid having to recompute performance deciles from historical data when wishing to refine a rating result, it is useful, depending on the actual size of the historical data, to initially compute performance quantiles with a relatively high number of bins, for instance *dodeciles* or *centiles*. It is then possible to correctly interpolate *quartiles* or *deciles* for instance, when constructing the rating digraph.

```
1>>> lqr2 = LearnedQuantilesRatingDigraph(pq1,newActions,
2... quantiles='quartiles')
3>>> lqr2.showQuantilesRating()
4 *-------- Deciles rating result ---------
5 [0.50 - 0.75[ ['a1005', 'a1010', 'a1002', 'a1008',
6 'a1006', 'a1001', 'a1003']
7 [0.25 - 0.50[ ['a1004', 'a1007', 'a1009']
```

With the *quantiles* parameter (see Listing 1.92 Line 2), we may recover by interpolation the same quartiles rating as obtained directly with historical performance quartiles (see Listing 1.90). Mind that a correct interpolation of quantiles from a given cumulative distribution function requires more or less uniform distributions of observations in each bin.

More generally, in the case of industrial production monitoring problems, for instance, where large volumes of historical performance data may be available, it may be of interest to estimate even more precisely the marginal cumulative distribution functions, especially when **tail** rating results, i.e. distinguishing **very best**, or **very worst** multiple criteria performances, become a critical issue. Similarly, the *historySize* parameter may be used for monitoring on the fly **unstable** random multiple criteria performance data.

Back to Content Table

## 1.14. The best students, where do they study? A *rating* case study

In 2004, the German magazine *Der Spiegel*, with the help of *McKinsey & Company* and *AOL*, conducted an extensive online survey, assessing the apparent quality of German University students 28. More than 80,000 students, by participating, were questioned on their ‘Abitur’ and university exams’ marks, time of studies and age, grants, awards and publications, IT proficiency, linguistic skills, practical work experience, foreign mobility and civil engagement. Each student received in return a *quality score* through a specific weighing of the collected data which depended on the subject the student is mainly studying. 29.

The eventually published results by the *Spiegel* magazine concerned nearly 50,000 students, enroled in one of fifteen popular academic subjects, like *German Studies*, *Life Sciences*, *Psychology*, *Law* or *CS*. Publishing only those subject-University combinations, where at least 18 students had correctly filled in the questionnaire, left 41 German Universities where, for at least eight out of the fifteen subjects, an average enrolment quality score could be determined 29.

Based on this published data 28, we would like to present and discuss in this tutorial, how to **rate** the apparent global *enrolment quality* of these 41 higher education institutions with the help of our *Digraph3* software ressources.

### 1.14.1. The performance tableau

Published data of the 2004 *Spiegel* student survey is stored, for our evaluation purpose here, in a file named studentenSpiegel04.py of `PerformanceTableau`

format 32.

```
1>>> from perfTabs import PerformanceTableau
2>>> t = PerformanceTableau('studentenSpiegel04')
3>>> t
4 *------- PerformanceTableau instance description ------*
5 Instance class : PerformanceTableau
6 Instance name : studentenSpiegel04
7 # Actions : 41 (Universities)
8 # Criteria : 15 (academic subjects)
9 NA proportion (%) : 27.3
10 Attributes : ['name', 'actions', 'objectives',
11 'criteria', 'weightPreorder',
12 'evaluation']
13>>> t.showHTMLPerformanceHeatmap(ndigits=1,\
14... rankingRule=None)
```

In Fig. 1.44, the fifteen popular academic subjects are grouped into topical ‘*Faculties*’: - *Humanities*; - *Law, Economics & Management*; - *Life Sciences & Medicine*; - *Natural Sciences & Mathematics*; and - *Technology*. All fifteen subjects are considered *equally significant* for our evaluation problem (see Row 2). The recorded average enrolment quality scores appear coloured along a 7-tiling scheme per subject (see last Row).

We may by the way notice that *TU Dresden* is the only Institution showing enrolment quality scores in all the fifteen academic subjects. Whereas, on the one side, *TU München* and *Kaiserslautern* are only valuated in *Sciences* and *Technology* subjects. On the other side, *Mannheim*, is only valuated in *Humanities* and *Law, Economics & Management* studies. Most of the 41 Universities are not valuated in *Engineering* studies. We are, hence, facing a large part (27.3%) of irreducible missing data (see Listing 1.93 Line 9 and the advanced topic on coping with missing data).

Details of the enrolment quality criteria (the academic subjects) may be consulted in a browser view (see Fig. 1.45 below).

```
>>> t.showHTMLCriteria()
```

The evaluation of the individual quality score for a participating student actually depends on his or her mainly enroled subject 29. The apparent quality measurement scales thus largely differ indeed from subject to subject (see Fig. 1.45), like *Law Studies* (35.0 - 65-0) and *Politology* (50.0 - 70.0). The recorded average enrolment quality scores, hence, are in fact **incommensurable** between the subjects.

To take furthermore into account a potential and very likely *imprecision* of the individual quality scores’ computation, we shall assume that, for all subjects, an average enrolment quality score difference of **0.1** is **insignificant**, wheras a difference of **0.5** is sufficient to *positively* attest a **better** enrolment quality.

The apparent *incommensurability* and very likely *imprecision* of the recorded average enrolment quality scores, renders **meaningless** any global averaging over the subjects per University of the enrolment quality. We shall therefore, similarly to the methodological approach of the *Spiegel* authors 29, proceed with an **order statistics** based *rating-by-ranking* approach (see tutorial on rating with learned quantile norms).

### 1.14.2. Rating-by-ranking with lower-closed quantile limits

The Spiegel authors opted indeed for a simple 3-tiling of the Universities per valuated academic subject, followed by an average *Borda* scores based global ranking 29. Here, our **epistemic logic** based **outranking approach**, allows us, with adequate choices of *indifference* (0.1) and *preference* (0.5) discrimination thresholds, to estimate **lower-closed 9-tiles** of the enrolment quality scores per subject and rank conjointly, with the help of the *Copeland* ranking rule 34 applied to a corresponding *bipolar-valued outranking* digraph, the 41 Universities **and** the lower limits of the estimated 9-tiles limits.

We need therefore to, first, estimate, with the help of the `PerformanceQuantiles`

constructor, the lowerclosed 9-tiling of the average enrolment quality scores per academic subject.

```
1>>> from performanceQuantiles import PerformanceQuantiles
2>>> pq = PerformanceQuantiles(t,numberOfBins=9,LowerClosed=True)
3>>> pq
4 *------- PerformanceQuantiles instance description ------*
5 Instance class : PerformanceQuantiles
6 Instance name : 9-tiled_performances
7 # Criteria : 15
8 # Quantiles : 9 (LowerClosed)
9 # History sizes : {'germ': 39, 'pol': 34, 'psy': 34, 'soc': 32,
10 'law': 32, 'eco': 21, 'mgt': 34,
11 'bio': 34, 'med': 28,
12 'phys': 37, 'chem': 35, 'math': 27,
13 'info': 33, 'elec': 14, 'mec': 13, }
```

The *history sizes*, reported in Listing 1.94 above, indicate the number of Universities valuated in each one of the popular fifteen subjects. *German Studies*, for instance, are valuated for 39 out of 41 Universities, whereas *Electrical* and *Mechanical Engineering* are only valuated for 14, respectively 13 Institutions. None of the fifteen subjects are valuated in all the 41 Universities 30.

We may inspect the resulting 9-tiling limits in a browser view.

```
>>> pq.showHTMLLimitingQuantiles(Transposed=True,Sorted=False,\
... ndigits=1,title='9-tiled quality score limits')
```

In Fig. 1.46, we see confirmed again the **incommensurability** between the subjects, we noticed already in the apparent enrolment quality scoring , especially between *Law Studies* (39.1 - 51.1) and *Politology* (50.5 - 65.9). Universities valuated in *Law studies* but not in *Politology*, like the University of *Bielefeld*, would see their enrolment quality *unfairly weakened* when simply averaging the enrolment quality scores over valuated subjects.

We add, now, these 9-tiling quality score limits to the enrolment quality records of the 41 Universities and rank all these records conjointly together with the help of the `LearnedQuantilesRatingDigraph`

constructor and by using the Copeland ranking rule.

```
>>> from sortingDigraphs import LearnedQuantilesRatingDigraph
>>> lqr = LearnedQuantilesRatingDigraph(pq,t,\
... rankingRule='Copeland')
```

The resulting ranking of the 41 Universities including the lower-closed 9-tiling score limits may be nicely illustrated with the help of a corresponding heatmap view (see Fig. 1.47).

```
>>> lqr.showHTMLRatingHeatmap(colorLevels=7,Correlations=True,\
... ndigits=1,rankingRule='Copeland')
```

The *ordinal correlation* (+0.967) 35 of the *Copeland ranking* with the underlying bipolar-valued outranking digraph is very high (see Fig. 1.47 Row 1). Most correlated subjects with this *rating-by-ranking* result appear to be *German Studies* (+0.51), *Chemistry* (+0.48), *Management* (+0.47) and *Physics* (+0.46). Both *Electrical* (+0.07) and *Mechanical Engineering* (+0.05) are the less correlated subjects (see Row 3).

From the actual ranking position of the lower 9-tiling limits, we may now immediately deduce the 9-tile enrolment quality equivalence classes. No University reaches the highest 9-tile (). In the lowest 9-tile () we find the University *Duisburg*. The complete rating result may be easily printed out as follows.

```
1>>> lqr.showQuantilesRating()
2 *-------- Quantiles rating result ---------
3 [0.89 - 1.00] []
4 [0.78 - 0.89[ ['tum', 'frei', 'kons', 'leip', 'mu', 'hei']
5 [0.67 - 0.78[ ['stu', 'berh']
6 [0.56 - 0.67[ ['aug', 'mnh', 'tueb', 'mnst', 'jena',
7 'reg', 'saar']
8 [0.44 - 0.56[ ['wrzb', 'dres', 'ksl', 'marb', 'berf',
9 'chem', 'koel', 'erl', 'tri']
10 [0.33 - 0.44[ ['goet', 'main', 'bon', 'brem']
11 [0.22 - 0.33[ ['fran', 'ham', 'kiel', 'aach',
12 'bertu', 'brau', 'darm']
13 [0.11 - 0.22[ ['gie', 'dsd', 'bie', 'boc', 'han']
14 [0.00 - 0.11[ ['duis']
```

Following Universities: *TU München*, *Freiburg*, *Konstanz*, *Leipzig*, *München* as well as *Heidelberg*, appear best rated in the eigth 9-tile (, see Listing 1.95 Line 4). Lowest-rated in the first 9-tile, as mentioned before, appears University *Duisburg* (Line 14). Midfield, the fifth 9-tile (), consists of the Universities *Würzburg*, *TU Dresden*, *Kaiserslautern*, *Marburg*, *FU Berlin*, *Chemnitz*, *Köln* , *Erlangen-Nürnberg* and *Trier* (Lines 8-9).

A corresponding *graphviz* drawing may well illustrate all these enrolment quality equivalence classes.

```
>>> lqr.exportRatingByRankingGraphViz(fileName='ratingResult',\
... graphSize='12,12')
*---- exporting a dot file for GraphViz tools ---------*
Exporting to ratingResult.dot
dot -Grankdir=TB -Tpdf dot -o ratingResult.png
```

We have noticed in the tutorial on ranking with multiple criteria, that there is not a single optimal rule for ranking from a given outranking digraph. The *Copeland* rule, for instance, has the advantage of being *Condorcet* consistent, i.e. when the outranking digraph models in fact a linear ranking, this ranking will necessarily be the result of the *Copeland* rule. When this is not the case, and especially when the outranking digraph shows many circuits, all potential ranking rules may give very divergent ranking results, and hence also substantially divergent rating-by-ranking results.

It is, hence, interesting, to verify if the epistemic fusion of the *rating-by-ranking* results, one may obtain when applying two different ranking rules, like the *Copeland* and the NetFlows ranking rule, does actually confirm our rating-by-ranking result shown in Fig. 1.48 above. For this purpose we make usage of the `RankingsFusionDigraph`

constructor (see Listing 1.96 Line 9).

```
1>>> lqr = LearnedQuantilesRatingDigraph(\
2... pq,t,rankingRule='Copeland')
3
4>>> lqr1 = LearnedQuantilesRatingDigraph(\
5... pq,t,rankingRule='NetFlows')
6
7>>> from transitiveDigraphs import\
8... RankingsFusionDigraph
9
10>>> rankings = [lqr.actionsRanking, \
11... lqr1.actionsRanking]
12
13>>> rf = RankingsFusionDigraph(lqr,rankings)
14>>> rf.exportGraphViz(fileName='fusionResult',\
15... WithRatingDecoration=True,\
16... graphSize='30,30')
17 *---- exporting a dot file for GraphViz tools ---------*
18 Exporting to fusionResult.dot
19 dot -Grankdir=TB -Tpng fusionResult.dot -o fusionResult.png
```

In Fig. 1.49 we notice that many Universities appear now rated into several adjacent 9-tiles. The previously best-rated Universities: *TU München*, *Freiburg*, *München*, *Leipzig*, as well as *Heidelberg*, for instance, appear now sorted into the *seventh* **and** *eigth* 9-tile (), whereas *Konstanz* is now, even **more imprecisely**, rated into the *sixth*, the *seventh* and the *eight* 9-tile.

How *confident*, hence, is our precise *Copeland* *rating-by-ranking* result? To investigate this question, let us now inspect the **outranking digraph** on which we actually apply the *Copeland* ranking rule.

### 1.14.3. Inspecting the bipolar-valued outranking digraph

We say that University *x* **outranks** (resp. **is outranked by**) University *y* in enrolment quality when there exists a **majority** (resp. only a **minority**) of valuated subjects showing an **at least as good as** average enrolment quality score.

To compute these outranking situations, we use the `BipolarOutrankingDigraph`

constructor.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> dg = BipolarOutrankingDigraph(t)
3>>> dg
4 *------- Object instance description ------*
5 Instance class : BipolarOutrankingDigraph
6 Instance name : rel_studentenSpiegel04
7 # Actions : 41 (Universities)
8 # Criteria : 15 (subjects)
9 Size : 828 (outranking situations)
10 Determinateness (%) : 63.67
11 Valuation domain : [-1.00;1.00]
12>>> dg.computeTransitivityDegree(Comments=True)
13 Transitivity degree of digraph <rel_studentenSpiegel04>:
14 #triples x>y>z: 57837, #closed: 30714, #open: 27123
15 (#closed/#triples) = 0.531
16>>> dg.computeSymmetryDegree(Comments=True)
17 Symmetry degree of digraph <rel_studentenSpiegel04>:
18 #arcs x>y: 793, #symmetric: 35, #asymmetric: 758
19 #symmetric/#arcs = 0.044
```

The bipolar-valued outranking digraph *dg* (see Listing 1.93 Line 2), obtained with the given performance tableau *t*, shows 828 positively validated pairwise outranking situations (Line 9). Unfortunately, the transitivity of digraph *dg* is far from being satisfied: nearly half of the transitive closure is missing (Line 15). Despite the rather large *preference discrimination* threshold (0.5) we have assumed (see Fig. 1.45), there does not occur many indifference situations (Line 19).

We may furthermore check if there exists any *cyclic* outranking situations.

```
1>>> dg.computeChordlessCircuits()
2>>> dg.showChordlessCircuits()
3 *---- Chordless circuits ----*
4 93 circuits.
5 1: ['aach', 'bie', 'darm', 'brau'] , credibility : 0.067
6 2: ['aach', 'bertu', 'brau'] , credibility : 0.200
7 3: ['aach', 'bertu', 'brem'] , credibility : 0.067
8 4: ['aach', 'bertu', 'ham'] , credibility : 0.200
9 5: ['aug', 'tri', 'marb'] , credibility : 0.067
10 6: ['aug', 'jena', 'marb'] , credibility : 0.067
11 7: ['aug', 'jena', 'koel'] , credibility : 0.067
12 ...
13 ...
14 29: ['berh', 'kons', 'mu'] , credibility : 0.133
15 ...
16 ...
17 88: ['main', 'mnh', 'marb'] , credibility : 0.067
18 89: ['marb', 'saar', 'wrzb'] , credibility : 0.067
19 90: ['marb', 'saar', 'reg'] , credibility : 0.067
20 91: ['marb', 'saar', 'mnst'] , credibility : 0.133
21 92: ['marb', 'saar', 'tri'] , credibility : 0.067
22 93: ['mnh', 'mu', 'stu'] , credibility : 0.133
```

Here we observe indeed 93 such outranking circuits, like: *Berlin Humboldt* > *Konstanz* > *München* > *Berlin Humboldt* supported by a (0.133 + 1.0)/2 = 56.7% majority of subjects 31 (see Listing 1.98 circuit 29 above). In the *Copeland* ranking result shown in Fig. 1.47, these Universities appear positioned respectively at ranks 10, 4 and 6. In the *NetFlows* ranking result they would appear respectively at ranks 10, 6 and 5, thus inverting the positions of *Konstanz* and *München*. The occurrence in digraph *dg* of so many outranking circuits makes thus *doubtful* any *forced* linear ranking, independently of the specific ranking rule we might have applied.

To effectively check the quality of our *Copeland* *rating-by-ranking* result, we shall now compute a direct **sorting into 9-tiles** of the enrolment quality scores, without using any outranking digraph based ranking rule.

### 1.14.4. Rating by quantiles sorting

In our case here, the Universities represent the decision actions: *where to study*. We say now that University *x* is sorted into the lower-closed 9-tile *q* when the performance record of *x* **positively outranks the lower limit** record of 9-tile *q* and *x* **does not positively outrank the upper limit** record of 9-tile *q*.

```
1>>> lqr.showActionsSortingResult()
2 Quantiles sorting result per decision action
3 [0.33 - 0.44[: aach with credibility: 0.13 = min(0.13,0.27)
4 [0.56 - 0.89[: aug with credibility: 0.13 = min(0.13,0.27)
5 [0.44 - 0.67[: berf with credibility: 0.13 = min(0.13,0.20)
6 [0.78 - 0.89[: berh with credibility: 0.13 = min(0.13,0.33)
7 [0.22 - 0.44[: bertu with credibility: 0.20 = min(0.33,0.20)
8 [0.11 - 0.22[: bie with credibility: 0.20 = min(0.33,0.20)
9 [0.22 - 0.33[: boc with credibility: 0.07 = min(0.07,0.07)
10 [0.44 - 0.56[: bon with credibility: 0.13 = min(0.20,0.13)
11 [0.33 - 0.44[: brau with credibility: 0.07 = min(0.07,0.27)
12 [0.33 - 0.44[: brem with credibility: 0.07 = min(0.07,0.07)
13 [0.44 - 0.56[: chem with credibility: 0.07 = min(0.13,0.07)
14 [0.22 - 0.56[: darm with credibility: 0.13 = min(0.13,0.13)
15 [0.56 - 0.67[: dres with credibility: 0.27 = min(0.27,0.47)
16 [0.22 - 0.33[: dsd with credibility: 0.07 = min(0.07,0.07)
17 [0.00 - 0.11[: duis with credibility: 0.33 = min(0.73,0.33)
18 [0.44 - 0.56[: erl with credibility: 0.13 = min(0.27,0.13)
19 [0.22 - 0.44[: fran with credibility: 0.13 = min(0.13,0.33)
20 [0.78 - <[: frei with credibility: 0.53 = min(0.53,1.00)
21 [0.22 - 0.33[: gie with credibility: 0.13 = min(0.13,0.20)
22 [0.33 - 0.44[: goet with credibility: 0.07 = min(0.47,0.07)
23 [0.22 - 0.33[: ham with credibility: 0.07 = min(0.33,0.07)
24 [0.11 - 0.22[: han with credibility: 0.20 = min(0.33,0.20)
25 [0.78 - 0.89[: hei with credibility: 0.13 = min(0.13,0.27)
26 [0.56 - 0.67[: jena with credibility: 0.07 = min(0.13,0.07)
27 [0.33 - 0.44[: kiel with credibility: 0.20 = min(0.20,0.47)
28 [0.44 - 0.56[: koel with credibility: 0.07 = min(0.27,0.07)
29 [0.78 - <[: kons with credibility: 0.20 = min(0.20,1.00)
30 [0.56 - 0.89[: ksl with credibility: 0.13 = min(0.13,0.40)
31 [0.78 - 0.89[: leip with credibility: 0.07 = min(0.20,0.07)
32 [0.44 - 0.56[: main with credibility: 0.07 = min(0.07,0.13)
33 [0.56 - 0.67[: marb with credibility: 0.07 = min(0.07,0.07)
34 [0.56 - 0.89[: mnh with credibility: 0.20 = min(0.20,0.27)
35 [0.56 - 0.67[: mnst with credibility: 0.07 = min(0.20,0.07)
36 [0.78 - 0.89[: mu with credibility: 0.13 = min(0.13,0.47)
37 [0.56 - 0.67[: reg with credibility: 0.20 = min(0.20,0.27)
38 [0.56 - 0.78[: saar with credibility: 0.13 = min(0.13,0.20)
39 [0.78 - 0.89[: stu with credibility: 0.07 = min(0.13,0.07)
40 [0.44 - 0.56[: tri with credibility: 0.07 = min(0.13,0.07)
41 [0.67 - 0.78[: tueb with credibility: 0.13 = min(0.13,0.20)
42 [0.89 - <[: tum with credibility: 0.13 = min(0.13,1.00)
43 [0.56 - 0.67[: wrzb with credibility: 0.07 = min(0.20,0.07)
```

In the 9-tiles sorting result, shown in Listing 1.99, we notice for instance in Lines 3-4 that the *RWTH Aachen* is precisely rated into the 4th 9-tile (), whereas the University *Augsburg* is less precisely rated conjointly into the *6th*, the *7th* and the *8th* 9-tile (). In Line 42, *TU München* appears best rated into the unique highest 9-tile (). All three rating results are supported by a (0.07 + 1.0)/2 = 53.5% majority of valuated subjects 31. With the support of a 76.5% majority of valuated subjects (Line 20), the apparent most confident rating result is the one of University *Freiburg* (see also Fig. 1.44 and Fig. 1.47).

We shall now lexicographically sort these individual rating results per University, by *average* rated 9-tile limits and *highest-rated* upper 9-tile limit, into ordered, but not necessarily disjoint, enrolment quality quantiles.

```
>>> lqr.showHTMLQuantilesSorting(strategy='average')
```

In Fig. 1.50 we may notice that the Universities: *Augsburg*, *Kaiserslautern*, *Mannheim* and *Tübingen* for instance, show in fact the same average rated 9-tiles score of 0.725; yet, the rated upper 9-tile limit of *Tuebingen* is only 0.78, whereas the one of the other Universities reaches 0.89. Hence, *Tuebingen* is ranked below *Augsburg*, *Kaiserslautern* and *Mannheim* .

With a special *graphviz* drawing of the `LearnedQuantilesRatingDigraph`

instance *lqr*, we may, without requiring any specific ordering strategy, as well illustrate our 9-tiles *rating-by-sorting* result.

```
>>> lqr.exportRatingBySortingGraphViz(\
... 'nineTilingDrawing',graphSize='12,12')
*---- exporting a dot file for GraphViz tools ---------*
Exporting to nineTilingDrawing.dot
dot -Grankdir=TB -Tpng nineTilingDrawing.dot -o nineTilingDrawing.png
```

In Fig. 1.51 we actually see the *skeleton* (transitive closure removed) of a **partial order**, where an oriented arc is drawn between Universities *x* and *y* when their 9-tiles sorting results are **disjoint** and the one of *x* is **higher rated** than the one of *y*. The rating for *TU München* (see Listing 1.99 Lines 45), for instance, is disjoint and higher rated than the one of the Universities *Freiburg* and *Konstanz* (Lines 23, 32). And, both the ratings of *Feiburg* and *Konstanz* are, however, not disjoint from the one, for instance, of the Universty of *Stuttgart* (Line 42).

The partial ranking, shown in Fig. 1.51, is in fact **independent** of any ordering strategy: - *average*, - *optimistic* or - *pessimistic*, of overlapping 9-tiles sorting results, and confirms that the same Universities as with the previous *rating-by-ranking* approach, namely *TU München*, *Freiburg*, *Konstanz*, *Stuttgart*, *Berlin Humboldt*, *Heidelberg* and *Leipzig* appear top-rated. Similarly, the Universities of *Duisburg*, *Bielefeld*, *Hanover*, *Bochum*, *Giessen*, *Düsseldorf* and *Hamburg* give the lowest-rated group. The midfield here is again consisting of more or less the same Universities as the one observed in the previous *rating-by-ranking* approach (see Fig. 1.48).

### 1.14.5. To conclude

In the end, both the *Copeland* *rating-by-ranking*, as well as the *rating-by-sorting* approach give luckily, in our case study here, very similar results. The first approach, with its *forced* linear ranking, determines on the one hand, *precise* enrolment quality equivalence classes; a result, depending potentially a lot on the actually applied ranking rule. The *rating-by-sorting* approach, on the other hand, only determines for each University a less precise but *prudent* rating of its individual enrolment quality, furthermore supported by a known majority of performance criteria significance; a somehow *fairer* and *robuster* result, but, much less evident for easily comparing the apparent enrolment quality among Universities. Contradictorily, or sparsely valuated Universities, for instance, will appear trivially rated into a large midfield of adjacent 9-tiles.

Let us conclude by saying that we prefer this latter *rating-by-sorting* approach; perhaps impreciser, due the case given, to missing and contradictory performance data; yet, well grounded in a powerful bipolar-valued logical and espistemic framework (see the advanced topics of the Digraph3 documentation).

Back to Content Table

## 1.15. HPC ranking with big outranking digraphs

### 1.15.1. C-compiled Python modules

The Digraph3 collection provides cythonized 6, i.e. C-compiled and optimised versions of the main python modules for tackling multiple criteria decision problems facing very large sets of decision alternatives ( > 10000 ). Such problems appear usually with a combinatorial organisation of the potential decision alternatives, as is frequently the case in bioinformatics for instance. If HPC facilities with nodes supporting numerous cores (> 20) and big RAM (> 50GB) are available, ranking up to several millions of alternatives (see [BIS-2016]) becomes effectively tractable.

Four cythonized Digraph3 modules, prefixed with the letter *c* and taking a *pyx* extension, are provided with their corresponding setup tools in the *Digraph3/cython* directory, namely

cRandPerfTabs.pyx

cIntegerOutrankingDigraphs.pyx

cIntegerSortingDigraphs.pyx

cSparseIntegerOutrankingDigraphs.pyx

Their automatic compilation and installation, alongside the standard Digraph3 python3 modules, requires the *cython* compiler 6 ( …$ pip3 install cython ) and a C compiler (…$ sudo apt install gcc on Ubuntu).

Warning

These cythonized modules, specifically designed for being run on HPC clusters (see https://hpc.uni.lu), require the Unix *forking* start method of subprocesses (see start methods of the multiprocessing module) and therefore, due to forking problems on Mac OS platforms, may only operate safely on Linux platforms.

### 1.15.2. Big Data performance tableaux

In order to efficiently type the C variables, the `cRandPerfTabs`

module provides the usual random performance tableau models, but, with **integer** action keys, **float** performance evaluations, **integer** criteria weights and **float** discrimination thresholds. And, to limit as much as possible memory occupation of class instances, all the usual verbose comments are dropped from the description of the *actions* and *criteria* dictionaries.

```
1>>> from cRandPerfTabs import *
2>>> t = cRandomPerformanceTableau(numberOfActions=4,numberOfCriteria=2)
3>>> t
4 *------- PerformanceTableau instance description ------*
5 Instance class : cRandomPerformanceTableau
6 Seed : None
7 Instance name : cRandomperftab
8 # Actions : 4
9 # Criteria : 2
10 Attributes : ['randomSeed', 'name', 'actions', 'criteria',
11 'evaluation', 'weightPreorder']
12>>> t.actions
13 OrderedDict([(1, {'name': '#1'}), (2, {'name': '#2'}),
14 (3, {'name': '#3'}), (4, {'name': '#4'})])
15>>> t.criteria
16 OrderedDict([
17 ('g1', {'name': 'RandomPerformanceTableau() instance',
18 'comment': 'Arguments: ; weightDistribution=equisignificant;
19 weightScale=(1, 1); commonMode=None',
20 'thresholds': {'ind': (10.0, 0.0),
21 'pref': (20.0, 0.0),
22 'veto': (80.0, 0.0)},
23 'scale': (0.0, 100.0),
24 'weight': 1,
25 'preferenceDirection': 'max'}),
26 ('g2', {'name': 'RandomPerformanceTableau() instance',
27 'comment': 'Arguments: ; weightDistribution=equisignificant;
28 weightScale=(1, 1); commonMode=None',
29 'thresholds': {'ind': (10.0, 0.0),
30 'pref': (20.0, 0.0),
31 'veto': (80.0, 0.0)},
32 'scale': (0.0, 100.0),
33 'weight': 1,
34 'preferenceDirection': 'max'})])
35>>> t.evaluation
36 {'g1': {1: 35.17, 2: 56.4, 3: 1.94, 4: 5.51},
37 'g2': {1: 95.12, 2: 90.54, 3: 51.84, 4: 15.42}}
38>>> t.showPerformanceTableau()
39 Criteria | 'g1' 'g2'
40 Actions | 1 1
41 ---------|---------------
42 '#1' | 91.18 90.42
43 '#2' | 66.82 41.31
44 '#3' | 35.76 28.86
45 '#4' | 7.78 37.64
```

Conversions from the Big Data model to the standard model and vice versa are provided.

```
1>>> t1 = t.convert2Standard()
2>>> t1.convertWeight2Decimal()
3>>> t1.convertEvaluation2Decimal()
4>>> t1
5 *------- PerformanceTableau instance description ------*
6 Instance class : PerformanceTableau
7 Seed : None
8 Instance name : std_cRandomperftab
9 # Actions : 4
10 # Criteria : 2
11 Attributes : ['name', 'actions', 'criteria', 'weightPreorder',
12 'evaluation', 'randomSeed']
```

### 1.15.3. C-implemented integer-valued outranking digraphs

The C compiled version of the bipolar-valued digraph models takes integer relation characteristic values.

```
1>>> t = cRandomPerformanceTableau(numberOfActions=1000,numberOfCriteria=2)
2>>> from cIntegerOutrankingDigraphs import *
3>>> g = IntegerBipolarOutrankingDigraph(t,Threading=True,nbrCores=4)
4>>> g
5 *------- Object instance description ------*
6 Instance class : IntegerBipolarOutrankingDigraph
7 Instance name : rel_cRandomperftab
8 # Actions : 1000
9 # Criteria : 2
10 Size : 465024
11 Determinateness : 56.877
12 Valuation domain : {'min': -2, 'med': 0, 'max': 2,
13 'hasIntegerValuation': True}
14 ---- Constructor run times (in sec.) ----
15 Total time : 4.23880
16 Data input : 0.01203
17 Compute relation : 3.60788
18 Gamma sets : 0.61889
19 #Threads : 4
20 Attributes : ['name', 'actions', 'criteria', 'totalWeight',
21 'valuationdomain', 'methodData', 'evaluation',
22 'order', 'runTimes', 'nbrThreads', 'relation',
23 'gamma', 'notGamma']
```

On a classic intel-i7 equipped PC with four single threaded cores, the `IntegerBipolarOutrankingDigraph`

constructor takes about four seconds for computing a **million** pairwise outranking characteristic values. In a similar setting, the standard `BipolarOutrankingDigraph`

class constructor operates more than two times slower.

```
1>>> from outrankingDigraphs import BipolarOutrankingDigraph
2>>> t1 = t.convert2Standard()
3>>> g1 = BipolarOutrankingDigraph(t1,Threading=True,nbrCores=4)
4>>> g1
5 *------- Object instance description ------*
6 Instance class : BipolarOutrankingDigraph
7 Instance name : rel_std_cRandomperftab
8 # Actions : 1000
9 # Criteria : 2
10 Size : 465024
11 Determinateness : 56.817
12 Valuation domain : {'min': Decimal('-1.0'),
13 'med': Decimal('0.0'),
14 'max': Decimal('1.0'),
15 'precision': Decimal('0')}
16 ---- Constructor run times (in sec.) ----
17 Total time : 8.63340
18 Data input : 0.01564
19 Compute relation : 7.52787
20 Gamma sets : 1.08987
21 #Threads : 4
```

By far, most of the run time is in each case needed for computing the individual pairwise outranking characteristic values. Notice also below the memory occupations of both outranking digraph instances.

```
1>>> from digraphsTools import total_size
2>>> total_size(g)
3 108662777
4>>> total_size(g1)
5 212679272
6>>> total_size(g.relation)/total_size(g)
7 0.34
8>>> total_size(g.gamma)/total_size(g)
9 0.45
```

About 103MB for *g* and 202MB for *g1*. The standard *Decimal* valued `BipolarOutrankingDigraph`

instance *g1* thus nearly doubles the memory occupation of the corresponding `IntegerBipolarOutrankingDigraph`

*g* instance (see Line 3 and 5 above). 3/4 of this memory occupation is due to the *g.relation* (34%) and the *g.gamma* (45%) dictionaries. And these ratios quadratically grow with the digraph order. To limit the object sizes for really big outranking digraphs, we need to abandon the complete implementation of adjacency tables and gamma functions.

### 1.15.4. The sparse outranking digraph implementation

The idea is to first decompose the complete outranking relation into an ordered collection of equivalent quantile performance classes. Let us consider for this illustration a random performance tableau with 100 decision alternatives evaluated on 7 criteria.

```
1>>> from cRandPerfTabs import *
2>>> t = cRandomPerformanceTableau(numberOfActions=100,\
3... numberOfCriteria=7,seed=100)
```

We sort the 100 decision alternatives into overlapping quartile classes and rank with respect to the average quantile limits.

```
1>>> from cSparseIntegerOutrankingDigraphs import *
2>>> sg = SparseIntegerOutrankingDigraph(t,quantiles=4)
3>>> sg
4 *----- Object instance description --------------*
5 Instance class : SparseIntegerOutrankingDigraph
6 Instance name : cRandomperftab_mp
7 # Actions : 100
8 # Criteria : 7
9 Sorting by : 4-Tiling
10 Ordering strategy : average
11 Ranking rule : Copeland
12 # Components : 6
13 Minimal order : 1
14 Maximal order : 35
15 Average order : 16.7
16 fill rate : 24.970%
17 *---- Constructor run times (in sec.) ----
18 Nbr of threads : 1
19 Total time : 0.08212
20 QuantilesSorting : 0.01481
21 Preordering : 0.00022
22 Decomposing : 0.06707
23 Ordering : 0.00000
24 Attributes : ['runTimes', 'name', 'actions', 'criteria',
25 'evaluation', 'order', 'dimension',
26 'sortingParameters', 'nbrOfCPUs',
27 'valuationdomain', 'profiles', 'categories',
28 'sorting', 'minimalComponentSize',
29 'decomposition', 'nbrComponents', 'nd',
30 'components', 'fillRate',
31 'maximalComponentSize', 'componentRankingRule',
32 'boostedRanking']
```

We obtain in this example here a decomposition into 6 linearly ordered components with a maximal component size of 35 for component *c3*.

```
1>>> sg.showDecomposition()
2 *--- quantiles decomposition in decreasing order---*
3 c1. ]0.75-1.00] : [3, 22, 24, 34, 41, 44, 50, 53, 56, 62, 93]
4 c2. ]0.50-1.00] : [7, 29, 43, 58, 63, 81, 96]
5 c3. ]0.50-0.75] : [1, 2, 5, 8, 10, 11, 20, 21, 25, 28, 30, 33,
6 35, 36, 45, 48, 57, 59, 61, 65, 66, 68, 70,
7 71, 73, 76, 82, 85, 89, 90, 91, 92, 94, 95, 97]
8 c4. ]0.25-0.75] : [17, 19, 26, 27, 40, 46, 55, 64, 69, 87, 98, 100]
9 c5. ]0.25-0.50] : [4, 6, 9, 12, 13, 14, 15, 16, 18, 23, 31, 32,
10 37, 38, 39, 42, 47, 49, 51, 52, 54, 60, 67, 72,
11 74, 75, 77, 78, 80, 86, 88, 99]
12 c6. ]<-0.25] : [79, 83, 84]
```

A restricted outranking relation is stored for each component with more than one alternative. The resulting global relation map of the first ranked 75 alternatives looks as follows.

```
>>> sg.showRelationMap(toIndex=75)
```

With a fill rate of 25%, the memory occupation of this sparse outranking digraph *sg* instance takes now only 769kB, compared to the 1.7MB required by a corresponding standard IntegerBipolarOutrankingDigraph instance.

```
>>> print('%.0fkB' % (total_size(sg)/1024) )
769kB
```

For sparse outranking digraphs, the adjacency table is implemented as a dynamic `relation()`

function instead of a double dictionary.

```
1 def relation(self, int x, int y):
2 """
3 *Parameters*:
4 * x (int action key),
5 * y (int action key).
6 Dynamic construction of the global outranking
7 characteristic function *r(x S y)*.
8 """
9 cdef int Min, Med, Max, rx, ry
10 Min = self.valuationdomain['min']
11 Med = self.valuationdomain['med']
12 Max = self.valuationdomain['max']
13 if x == y:
14 return Med
15 cx = self.actions[x]['component']
16 cy = self.actions[y]['component']
17 #print(self.components)
18 rx = self.components[cx]['rank']
19 ry = self.components[cy]['rank']
20 if rx == ry:
21 try:
22 rxpg = self.components[cx]['subGraph'].relation
23 return rxpg[x][y]
24 except AttributeError:
25 componentRanking = self.components[cx]['componentRanking']
26 if componentRanking.index(x) < componentRanking.index(x):
27 return Max
28 else:
29 return Min
30 elif rx > ry:
31 return Min
32 else:
33 return Max
```

### 1.15.5. Ranking big sets of decision alternatives

We may now rank the complete set of 100 decision alternatives by locally ranking with the *Copeland* or the *NetFlows* rule, for instance, all these individual components.

```
1>>> sg.boostedRanking
2 [22, 53, 3, 34, 56, 62, 24, 44, 50, 93, 41, 63, 29, 58,
3 96, 7, 43, 81, 91, 35, 25, 76, 66, 65, 8, 10, 1, 11, 61,
4 30, 48, 45, 68, 5, 89, 57, 59, 85, 82, 73, 33, 94, 70,
5 97, 20, 92, 71, 90, 95, 21, 28, 2, 36, 87, 40, 98, 46, 55,
6 100, 64, 17, 26, 27, 19, 69, 6, 38, 4, 37, 60, 31, 77, 78,
7 47, 99, 18, 12, 80, 54, 88, 39, 9, 72, 86, 42, 13, 23, 67,
8 52, 15, 32, 49, 51, 74, 16, 14, 75, 79, 83, 84]
```

When actually computing linear rankings of a set of alternatives, the local outranking relations are of no practical usage, and we may furthermore reduce the memory occupation of the resulting digraph by

refining the ordering of the quantile classes by taking into account how well an alternative is outranking the lower limit of its quantile class, respectively the upper limit of its quantile class is

notoutranking the alternative;dropping the local outranking digraphs and keeping for each quantile class only a locally ranked list of alternatives.

We provide therefore the `cQuantilesRankingDigraph`

class.

```
1>>> qr = cQuantilesRankingDigraph(t,4)
2>>> qr
3 *----- Object instance description --------------*
4 Instance class : cQuantilesRankingDigraph
5 Instance name : cRandomperftab_mp
6 # Actions : 100
7 # Criteria : 7
8 Sorting by : 4-Tiling
9 Ordering strategy : optimal
10 Ranking rule : Copeland
11 # Components : 47
12 Minimal order : 1
13 Maximal order : 10
14 Average order : 2.1
15 fill rate : 2.566%
16 *---- Constructor run times (in sec.) ----*
17 Nbr of threads : 1
18 Total time : 0.03702
19 QuantilesSorting : 0.01785
20 Preordering : 0.00022
21 Decomposing : 0.01892
22 Ordering : 0.00000
23 Attributes : ['runTimes', 'name', 'actions', 'order',
24 'dimension', 'sortingParameters', 'nbrOfCPUs',
25 'valuationdomain', 'profiles', 'categories',
26 'sorting', 'minimalComponentSize',
27 'decomposition', 'nbrComponents', 'nd',
28 'components', 'fillRate', 'maximalComponentSize',
29 'componentRankingRule', 'boostedRanking']
```

With this *optimised* quantile ordering strategy, we obtain now 47 performance equivalence classes.

```
1>>> qr.components
2 OrderedDict([
3 ('c01', {'rank': 1,
4 'lowQtileLimit': ']0.75',
5 'highQtileLimit': '1.00]',
6 'componentRanking': [53]}),
7 ('c02', {'rank': 2,
8 'lowQtileLimit': ']0.75',
9 'highQtileLimit': '1.00]',
10 'componentRanking': [3, 23, 63, 50]}),
11 ('c03', {'rank': 3,
12 'lowQtileLimit': ']0.75',
13 'highQtileLimit': '1.00]',
14 'componentRanking': [34, 44, 56, 24, 93, 41]}),
15 ...
16 ...
17 ...
18 ('c45', {'rank': 45,
19 'lowQtileLimit': ']0.25',
20 'highQtileLimit': '0.50]',
21 'componentRanking': [49]}),
22 ('c46', {'rank': 46,
23 'lowQtileLimit': ']0.25',
24 'highQtileLimit': '0.50]',
25 'componentRanking': [52, 16, 86]}),
26 ('c47', {'rank': 47,
27 'lowQtileLimit': ']<',
28 'highQtileLimit': '0.25]',
29 'componentRanking': [79, 83, 84]})])
30>>> print('%.0fkB' % (total_size(qr)/1024) )
31 208kB
```

We observe an even more considerably less voluminous memory occupation: 208kB compared to the 769kB of the SparseIntegerOutrankingDigraph instance. It is opportune, however, to measure the loss of quality of the resulting *Copeland* ranking when working with sparse outranking digraphs.

```
1>>> from cIntegerOutrankingDigraphs import *
2>>> ig = IntegerBipolarOutrankingDigraph(t)
3>>> print('Complete outranking : %+.4f'\
4... % (ig.computeOrderCorrelation(ig.computeCopelandOrder())\
5... ['correlation']))
6
7 Complete outranking : +0.7474
8>>> print('Sparse 4-tiling : %+.4f'\
9... % (ig.computeOrderCorrelation(\
10... list(reversed(sg.boostedRanking)))['correlation']))
11
12 Sparse 4-tiling : +0.7172
13>>> print('Optimzed sparse 4-tiling: %+.4f'\
14... % (ig.computeOrderCorrelation(\
15... list(reversed(qr.boostedRanking)))['correlation']))
16
17 Optimzed sparse 4-tiling: +0.7051
```

The best ranking correlation with the pairwise outranking situations (+0.75) is naturally given when we apply the *Copeland* rule to the complete outranking digraph. When we apply the same rule to the sparse 4-tiled outranking digraph, we get a correlation of +0.72, and when applying the *Copeland* rule to the optimised 4-tiled digraph, we still obtain a correlation of +0.71. These results actually depend on the number of quantiles we use as well as on the given model of random performance tableau. In case of Random3ObjectivesPerformanceTableau instances, for instance, we would get in a similar setting a complete outranking correlation of +0.86, a sparse 4-tiling correlation of +0.82, and an optimzed sparse 4-tiling correlation of +0.81.

### 1.15.6. HPC quantiles ranking records

Following from the separability property of the *q*-tiles sorting of each action into each *q*-tiles class, the *q*-sorting algorithm may be safely split into as much threads as are multiple processing cores available in parallel. Furthermore, the ranking procedure being local to each diagonal component, these procedures may as well be safely processed in parallel threads on each component restricted outrankingdigraph.

Using the HPC platform of the University of Luxembourg (https://hpc.uni.lu/), the following run times for very big ranking problems could be achieved both:

by running the cythonized python modules in an Intel compiled virtual Python 3.6.5 environment [GCC Intel(R) 17.0.1 –enable-optimizations c++ gcc 6.3 mode] on Debian 8 Linux.

Example python session on the HPC-UL Iris-126 -skylake node 7

```
1 (myPy365ICC) [rbisdorff@iris-126 Test]$ python
2 Python 3.6.5 (default, May 9 2018, 09:54:28)
3 [GCC Intel(R) C++ gcc 6.3 mode] on linux
4 Type "help", "copyright", "credits" or "license" for more information.
5 >>>
```

```
1>>> from cRandPerfTabs import\
2... cRandom3ObjectivesPerformanceTableau as cR3ObjPT
3
4>>> pt = cR3ObjPT(numberOfActions=1000000,\
5... numberOfCriteria=21,\
6... weightDistribution='equiobjectives',\
7... commonScale = (0.0,1000.0),\
8... commonThresholds = [(2.5,0.0),(5.0,0.0),(75.0,0.0)],\
9... commonMode = ['beta','variable',None], \
10... missingDataProbability=0.05,\
11... seed=16)
12
13>>> import cSparseIntegerOutrankingDigraphs as iBg
14>>> qr = iBg.cQuantilesRankingDigraph(pt,quantiles=10,\
15... quantilesOrderingStrategy='optimal',\
16... minimalComponentSize=1,\
17... componentRankingRule='NetFlows',\
18... LowerClosed=False,\
19... Threading=True,\
20... tempDir='/tmp',\
21... nbrOfCPUs=28)
22
23>>> qr
24 *----- Object instance description --------------*
25 Instance class : cQuantilesRankingDigraph
26 Instance name : random3ObjectivesPerfTab_mp
27 # Actions : 1000000
28 # Criteria : 21
29 Sorting by : 10-Tiling
30 Ordering strategy : optimal
31 Ranking rule : NetFlows
32 # Components : 233645
33 Minimal order : 1
34 Maximal order : 153
35 Average order : 4.3
36 fill rate : 0.001%
37 *---- Constructor run times (in sec.) ----*
38 Nbr of threads : 28
39 Total time : 177.02770
40 QuantilesSorting : 99.55377
41 Preordering : 5.17954
42 Decomposing : 72.29356
```

On this 2x14c Intel Xeon Gold 6132 @ 2.6 GHz equipped HPC node with 132GB RAM 7, deciles sorting and locally ranking a **million** decision alternatives evaluated on 21 incommensurable criteria, by balancing an economic, an environmental and a societal decision objective, takes us about **3 minutes** (see Lines 37-42 above); with 1.5 minutes for the deciles sorting and, a bit more than one minute, for the local ranking of the individual components.

The optimised deciles sorting leads to 233645 components (see Lines 32-36 above) with a maximal order of 153. The fill rate of the adjacency table is reduced to 0.001%. Of the potential trillion (10^12) pairwise outrankings, we effectively keep only 10 millions (10^7). This high number of components results from the high number of involved performance criteria (21), leading in fact to a very refined epistemic discrimination of majority outranking margins.

A non-optimised deciles sorting would instead give at most 110 components with inevitably very big intractable local digraph orders. Proceeding with a more detailed quantiles sorting, for reducing the induced decomposing run times, leads however quickly to intractable quantiles sorting times. A good compromise is given when the quantiles sorting and decomposing steps show somehow equivalent run times; as is the case in our example session: 99.6 versus 77.3 seconds (see Lines 40 and 42 above).

Let us inspect the 21 marginal performances of the five best-ranked alternatives listed below.

```
1>>> pt.showPerformanceTableau(\
2... actionsSubset=qr.boostedRanking[:5],\
3... Transposed=True)
4
5*---- performance tableau -----*
6 criteria | weights | #773909 #668947 #567308 #578560 #426464
7 ---------|-------------------------------------------------------
8 'Ec01' | 42 | 969.81 844.71 917.00 NA 808.35
9 'So02' | 48 | NA 891.52 836.43 NA 899.22
10 'En03' | 56 | 687.10 NA 503.38 873.90 NA
11 'So04' | 48 | 455.05 845.29 866.16 800.39 956.14
12 'En05' | 56 | 809.60 846.87 939.46 851.83 950.51
13 'Ec06' | 42 | 919.62 802.45 717.39 832.44 974.63
14 'Ec07' | 42 | 889.01 722.09 606.11 902.28 574.08
15 'So08' | 48 | 862.19 699.38 907.34 571.18 943.34
16 'En09' | 56 | 857.34 817.44 819.92 674.60 376.70
17 'Ec10' | 42 | NA 874.86 NA 847.75 739.94
18 'En11' | 56 | NA 824.24 855.76 NA 953.77
19 'Ec12' | 42 | 802.18 871.06 488.76 841.41 599.17
20 'En13' | 56 | 827.73 839.70 864.48 720.31 877.23
21 'So14' | 48 | 943.31 580.69 827.45 815.18 461.04
22 'En15' | 56 | 794.57 801.44 924.29 938.70 863.72
23 'Ec16' | 42 | 581.15 599.87 949.84 367.34 859.70
24 'So17' | 48 | 881.55 856.05 NA 796.10 655.37
25 'Ec18' | 42 | 863.44 520.24 919.75 865.14 914.32
26 'So19' | 48 | NA NA NA 790.43 842.85
27 'Ec20' | 42 | 582.52 831.93 820.92 881.68 864.81
28 'So21' | 48 | 880.87 NA 628.96 746.67 863.82
```

The given ranking problem involves 8 criteria assessing the economic performances, 7 criteria assessing the societal performances and 6 criteria assessing the environmental performances of the decision alternatives. The sum of criteria significance weights (336) is the same for all three decision objectives. The five best-ranked alternatives are, in decreasing order: #773909, #668947, #567308, #578560 and #426464.

Their random performance evaluations were obviously drawn on all criteria with a *good* (+) performance profile, i.e. a Beta(*alpha* = 5.8661, *beta* = 2.62203) law (see the tutorial generating random performance tableaux).

```
1>>> for x in qr.boostedRanking[:5]:
2... print(pt.actions[x]['name'],\
3... pt.actions[x]['profile'])
4
5 #773909 {'Eco': '+', 'Soc': '+', 'Env': '+'}
6 #668947 {'Eco': '+', 'Soc': '+', 'Env': '+'}
7 #567308 {'Eco': '+', 'Soc': '+', 'Env': '+'}
8 #578560 {'Eco': '+', 'Soc': '+', 'Env': '+'}
9 #426464 {'Eco': '+', 'Soc': '+', 'Env': '+'}
```

We consider now a partial performance tableau *best10*, consisting only, for instance, of the **ten best-ranked alternatives**, with which we may compute a corresponding integer outranking digraph valued in the range (-1008, +1008).

```
1>>> best10 = cPartialPerformanceTableau(pt,qr.boostedRanking[:10])
2>>> from cIntegerOutrankingDigraphs import *
3>>> g = IntegerBipolarOutrankingDigraph(best10)
4>>> g.valuationdomain
5 {'min': -1008, 'med': 0, 'max': 1008, 'hasIntegerValuation': True}
6>>> g.showRelationTable(ReflexiveTerms=False)
7 * ---- Relation Table -----
8 r(x>y) | #773909 #668947 #567308 #578560 #426464 #298061 #155874 #815552 #279729 #928564
9 --------|-----------------------------------------------------------------------------------
10 #773909 | - +390 +90 +270 -50 +340 +220 +60 +116 +222
11 #668947 | +78 - +42 +250 -22 +218 +56 +172 +74 +64
12 #567308 | +70 +418 - +180 +156 +174 +266 +78 +256 +306
13 #578560 | -4 +78 +28 - -12 +100 -48 +154 -110 -10
14 #426464 | +202 +258 +284 +138 - +416 +312 +382 +534 +278
15 #298061 | -48 +68 +172 +32 -42 - +54 +48 +248 +374
16 #155874 | +72 +378 +322 +174 +274 +466 - +212 +308 +418
17 #815552 | +78 +126 +272 +318 +54 +194 +172 - -14 +22
18 #279729 | +240 +230 -110 +290 +72 +140 +388 +62 - +250
19 #928564 | +22 +228 -14 +246 +36 +78 +56 +110 +318 -
20 r(x>y) image range := [-1008;+1008]
21>>> g.condorcetWinners()
22 [155874, 426464, 567308]
23>>> g.computeChordlessCircuits()
24 []
25>>> g.computeTransitivityDegree()
26 0.78
```

Three alternatives -#155874, #426464 and #567308- qualify as Condorcet winners, i.e. they each **positively outrank** all the other nine alternatives. No chordless outranking circuits are detected, yet the transitivity of the apparent outranking relation is not given. And, no clear ranking alignment hence appears when inspecting the *strict* outranking digraph (i.e. the codual ~(-*g*) of *g*) shown in Fig. 1.54.

```
1>>> (~(-g)).exportGraphViz()
2*---- exporting a dot file for GraphViz tools ---------*
3 Exporting to converse-dual_rel_best10.dot
4 dot -Tpng converse-dual_rel_best10.dot -o converse-dual_rel_best10.png
```

Restricted to these ten best-ranked alternatives, the *Copeland*, the *NetFlows* as well as the *Kemeny* ranking rule will all rank alternative #426464 first and alternative #578560 last. Otherwise the three ranking rules produce in this case more or less different rankings.

```
1>>> g.computeCopelandRanking()
2 [426464, 567308, 155874, 279729, 773909, 928564, 668947, 815552, 298061, 578560]
3>>> g.computeNetFlowsRanking()
4 [426464, 155874, 773909, 567308, 815552, 279729, 928564, 298061, 668947, 578560]
5>>> from linearOrders import *
6>>> ke = KemenyOrder(g,orderLimit=10)
7>>> ke.kemenyRanking
8 [426464, 773909, 155874, 815552, 567308, 298061, 928564, 279729, 668947, 578560]
```

Note

It is therefore *important* to always keep in mind that, based on pairwise outranking situations, there **does not exist** any **unique optimal ranking**; especially when we face such big data problems. Changing the number of quantiles, the component ranking rule, the optimised quantile ordering strategy, all this will indeed produce, sometimes even substantially, diverse global ranking results.

Back to Content Table

## 1.16. Working with the `graphs`

module

See also

The technical documentation of the graphs module.

### 1.16.1. Structure of a `Graph`

object

In the `graphs`

module, the root `Graph`

class provides a generic **simple graph model**, without loops and multiple links. A given object of this class consists in:

the graph

**vertices**: a dictionary of vertices with ‘name’ and ‘shortName’ attributes,the graph

**valuationDomain**, a dictionary with three entries: the minimum (-1, means certainly no link), the median (0, means missing information) and the maximum characteristic value (+1, means certainly a link),the graph

**edges**: a dictionary with frozensets of pairs of vertices as entries carrying a characteristic value in the range of the previous valuation domain,and its associated

**gamma function**: a dictionary containing the direct neighbors of each vertex, automatically added by the object constructor.

See the technical documentation of the graphs module.

Example Python3 session

```
1>>> from graphs import Graph
2>>> g = Graph(numberOfVertices=7,edgeProbability=0.5)
3>>> g.save(fileName='tutorialGraph')
```

The saved `Graph`

instance named ‘tutorialGraph.py’ is encoded in python3 as follows.

```
1 # Graph instance saved in Python format
2 vertices = {
3 'v1': {'shortName': 'v1', 'name': 'random vertex'},
4 'v2': {'shortName': 'v2', 'name': 'random vertex'},
5 'v3': {'shortName': 'v3', 'name': 'random vertex'},
6 'v4': {'shortName': 'v4', 'name': 'random vertex'},
7 'v5': {'shortName': 'v5', 'name': 'random vertex'},
8 'v6': {'shortName': 'v6', 'name': 'random vertex'},
9 'v7': {'shortName': 'v7', 'name': 'random vertex'},
10 }
11 valuationDomain = {'min':-1,'med':0,'max':1}
12 edges = {
13 frozenset(['v1','v2']) : -1,
14 frozenset(['v1','v3']) : -1,
15 frozenset(['v1','v4']) : -1,
16 frozenset(['v1','v5']) : 1,
17 frozenset(['v1','v6']) : -1,
18 frozenset(['v1','v7']) : -1,
19 frozenset(['v2','v3']) : 1,
20 frozenset(['v2','v4']) : 1,
21 frozenset(['v2','v5']) : -1,
22 frozenset(['v2','v6']) : 1,
23 frozenset(['v2','v7']) : -1,
24 frozenset(['v3','v4']) : -1,
25 frozenset(['v3','v5']) : -1,
26 frozenset(['v3','v6']) : -1,
27 frozenset(['v3','v7']) : -1,
28 frozenset(['v4','v5']) : 1,
29 frozenset(['v4','v6']) : -1,
30 frozenset(['v4','v7']) : 1,
31 frozenset(['v5','v6']) : 1,
32 frozenset(['v5','v7']) : -1,
33 frozenset(['v6','v7']) : -1,
34 }
```

The stored graph can be recalled and plotted with the generic `exportGraphViz()`

1 method as follows.

```
1>>> g = Graph('tutorialGraph')
2>>> g.exportGraphViz()
3 *---- exporting a dot file for GraphViz tools ---------*
4 Exporting to tutorialGraph.dot
5 fdp -Tpng tutorialGraph.dot -o tutorialGraph.png
```

Properties, like the gamma function and vertex degrees and neighbourhood depths may be shown with a graphs.Graph.showShort() method.

```
1>>> g.showShort()
2 *---- short description of the graph ----*
3 Name : 'tutorialGraph'
4 Vertices : ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
5 Valuation domain : {'min': -1, 'med': 0, 'max': 1}
6 Gamma function :
7 v1 -> ['v5']
8 v2 -> ['v6', 'v4', 'v3']
9 v3 -> ['v2']
10 v4 -> ['v5', 'v2', 'v7']
11 v5 -> ['v1', 'v6', 'v4']
12 v6 -> ['v2', 'v5']
13 v7 -> ['v4']
14 degrees : [0, 1, 2, 3, 4, 5, 6]
15 distribution : [0, 3, 1, 3, 0, 0, 0]
16 nbh depths : [0, 1, 2, 3, 4, 5, 6, 'inf.']
17 distribution : [0, 0, 1, 4, 2, 0, 0, 0]
```

A `Graph`

instance corresponds bijectively to a symmetric `Digraph`

instance and we may easily convert from one to the other with the `graph2Digraph()`

, and vice versa with the `digraph2Graph()`

method. Thus, all resources of the `Digraph`

class, suitable for symmetric digraphs, become readily available, and vice versa.

```
1>>> dg = g.graph2Digraph()
2>>> dg.showRelationTable(ndigits=0,ReflexiveTerms=False)
3 * ---- Relation Table -----
4 S | 'v1' 'v2' 'v3' 'v4' 'v5' 'v6' 'v7'
5 -----|------------------------------------------
6 'v1' | - -1 -1 -1 1 -1 -1
7 'v2' | -1 - 1 1 -1 1 -1
8 'v3' | -1 1 - -1 -1 -1 -1
9 'v4' | -1 1 -1 - 1 -1 1
10 'v5' | 1 -1 -1 1 - 1 -1
11 'v6' | -1 1 -1 -1 1 - -1
12 'v7' | -1 -1 -1 1 -1 -1 -
13>>> g1 = dg.digraph2Graph()
14>>> g1.showShort()
15 *---- short description of the graph ----*
16 Name : 'tutorialGraph'
17 Vertices : ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
18 Valuation domain : {'med': 0, 'min': -1, 'max': 1}
19 Gamma function :
20 v1 -> ['v5']
21 v2 -> ['v3', 'v6', 'v4']
22 v3 -> ['v2']
23 v4 -> ['v5', 'v7', 'v2']
24 v5 -> ['v6', 'v1', 'v4']
25 v6 -> ['v5', 'v2']
26 v7 -> ['v4']
27 degrees : [0, 1, 2, 3, 4, 5, 6]
28 distribution : [0, 3, 1, 3, 0, 0, 0]
29 nbh depths : [0, 1, 2, 3, 4, 5, 6, 'inf.']
30 distribution : [0, 0, 1, 4, 2, 0, 0, 0]
```

### 1.16.2. q-coloring of a graph

A 3-coloring of the tutorial graph *g* may for instance be computed and plotted with the `Q_Coloring`

class as follows.

```
1>>> from graphs import Q_Coloring
2>>> qc = Q_Coloring(g)
3 Running a Gibbs Sampler for 42 step !
4 The q-coloring with 3 colors is feasible !!
5>>> qc.showConfiguration()
6 v5 lightblue
7 v3 gold
8 v7 gold
9 v2 lightblue
10 v4 lightcoral
11 v1 gold
12 v6 lightcoral
13>>> qc.exportGraphViz('tutorial-3-coloring')
14 *---- exporting a dot file for GraphViz tools ---------*
15 Exporting to tutorial-3-coloring.dot
16 fdp -Tpng tutorial-3-coloring.dot -o tutorial-3-coloring.png
```

Actually, with the given tutorial graph instance, a 2-coloring is already feasible.

```
1>>> qc = Q_Coloring(g,colors=['gold','coral'])
2 Running a Gibbs Sampler for 42 step !
3 The q-coloring with 2 colors is feasible !!
4>>> qc.showConfiguration()
5 v5 gold
6 v3 coral
7 v7 gold
8 v2 gold
9 v4 coral
10 v1 coral
11 v6 coral
12 >>> qc.exportGraphViz('tutorial-2-coloring')
13 Exporting to tutorial-2-coloring.dot
14 fdp -Tpng tutorial-2-coloring.dot -o tutorial-2-coloring.png
```

### 1.16.3. MIS and clique enumeration

2-colorings define independent sets of vertices that are maximal in cardinality; for short called a **MIS**. Computing such MISs in a given `Graph`

instance may be achieved by the `showMIS()`

method.

```
1>>> g = Graph('tutorialGraph')
2>>> g.showMIS()
3 *--- Maximal Independent Sets ---*
4 ['v2', 'v5', 'v7']
5 ['v3', 'v5', 'v7']
6 ['v1', 'v2', 'v7']
7 ['v1', 'v3', 'v6', 'v7']
8 ['v1', 'v3', 'v4', 'v6']
9 number of solutions: 5
10 cardinality distribution
11 card.: [0, 1, 2, 3, 4, 5, 6, 7]
12 freq.: [0, 0, 0, 3, 2, 0, 0, 0]
13 execution time: 0.00032 sec.
14 Results in self.misset
15>>> g.misset
16 [frozenset({'v7', 'v2', 'v5'}),
17 frozenset({'v3', 'v7', 'v5'}),
18 frozenset({'v1', 'v2', 'v7'}),
19 frozenset({'v1', 'v6', 'v7', 'v3'}),
20 frozenset({'v1', 'v6', 'v4', 'v3'})]
```

A MIS in the dual of a graph instance *g* (its negation *-g* 14), corresponds to a maximal **clique**, i.e. a maximal complete subgraph in *g*. Maximal cliques may be directly enumerated with the `showCliques()`

method.

```
1>>> g.showCliques()
2 *--- Maximal Cliques ---*
3 ['v2', 'v3']
4 ['v4', 'v7']
5 ['v2', 'v4']
6 ['v4', 'v5']
7 ['v1', 'v5']
8 ['v2', 'v6']
9 ['v5', 'v6']
10 number of solutions: 7
11 cardinality distribution
12 card.: [0, 1, 2, 3, 4, 5, 6, 7]
13 freq.: [0, 0, 7, 0, 0, 0, 0, 0]
14 execution time: 0.00049 sec.
15 Results in self.cliques
16>>> g.cliques
17 [frozenset({'v2', 'v3'}), frozenset({'v4', 'v7'}),
18 frozenset({'v2', 'v4'}), frozenset({'v4', 'v5'}),
19 frozenset({'v1', 'v5'}), frozenset({'v6', 'v2'}),
20 frozenset({'v6', 'v5'})]
```

### 1.16.4. Line graphs and maximal matchings

The module also provides a `LineGraph`

constructor. A **line graph** represents the **adjacencies between edges** of the given graph instance. We may compute for instance the line graph of the 5-cycle graph.

```
1>>> from graphs import CycleGraph, LineGraph
2>>> g = CycleGraph(order=5)
3>>> g
4 *------- Graph instance description ------*
5 Instance class : CycleGraph
6 Instance name : cycleGraph
7 Graph Order : 5
8 Graph Size : 5
9 Valuation domain : [-1.00; 1.00]
10 Attributes : ['name', 'order', 'vertices', 'valuationDomain',
11 'edges', 'size', 'gamma']
12>>> lg = LineGraph(g)
13>>> lg
14 *------- Graph instance description ------*
15 Instance class : LineGraph
16 Instance name : line-cycleGraph
17 Graph Order : 5
18 Graph Size : 5
19 Valuation domain : [-1.00; 1.00]
20 Attributes : ['name', 'graph', 'valuationDomain', 'vertices',
21 'order', 'edges', 'size', 'gamma']
22>>> lg.showShort()
23 *---- short description of the graph ----*
24 Name : 'line-cycleGraph'
25 Vertices : [frozenset({'v1', 'v2'}), frozenset({'v1', 'v5'}), frozenset({'v2', 'v3'}),
26 frozenset({'v3', 'v4'}), frozenset({'v4', 'v5'})]
27 Valuation domain : {'min': Decimal('-1'), 'med': Decimal('0'), 'max': Decimal('1')}
28 Gamma function :
29 frozenset({'v1', 'v2'}) -> [frozenset({'v2', 'v3'}), frozenset({'v1', 'v5'})]
30 frozenset({'v1', 'v5'}) -> [frozenset({'v1', 'v2'}), frozenset({'v4', 'v5'})]
31 frozenset({'v2', 'v3'}) -> [frozenset({'v1', 'v2'}), frozenset({'v3', 'v4'})]
32 frozenset({'v3', 'v4'}) -> [frozenset({'v2', 'v3'}), frozenset({'v4', 'v5'})]
33 frozenset({'v4', 'v5'}) -> [frozenset({'v4', 'v3'}), frozenset({'v1', 'v5'})]
34 degrees : [0, 1, 2, 3, 4]
35 distribution : [0, 0, 5, 0, 0]
36 nbh depths : [0, 1, 2, 3, 4, 'inf.']
37 distribution : [0, 0, 5, 0, 0, 0]
```

Iterated line graph constructions are usually expanding, except for *chordless cycles*, where the same cycle is repeated, and for *non-closed paths*, where iterated line graphs progressively reduce one by one the number of vertices and edges and become eventually an empty graph.

Notice that the MISs in the line graph provide **maximal matchings** - *maximal sets of independent edges* - of the original graph.

```
1>>> c8 = CycleGraph(order=8)
2>>> lc8 = LineGraph(c8)
3>>> lc8.showMIS()
4 *--- Maximal Independent Sets ---*
5 [frozenset({'v3', 'v4'}), frozenset({'v5', 'v6'}), frozenset({'v1', 'v8'})]
6 [frozenset({'v2', 'v3'}), frozenset({'v5', 'v6'}), frozenset({'v1', 'v8'})]
7 [frozenset({'v8', 'v7'}), frozenset({'v2', 'v3'}), frozenset({'v5', 'v6'})]
8 [frozenset({'v8', 'v7'}), frozenset({'v2', 'v3'}), frozenset({'v4', 'v5'})]
9 [frozenset({'v7', 'v6'}), frozenset({'v3', 'v4'}), frozenset({'v1', 'v8'})]
10 [frozenset({'v2', 'v1'}), frozenset({'v8', 'v7'}), frozenset({'v4', 'v5'})]
11 [frozenset({'v2', 'v1'}), frozenset({'v7', 'v6'}), frozenset({'v4', 'v5'})]
12 [frozenset({'v2', 'v1'}), frozenset({'v7', 'v6'}), frozenset({'v3', 'v4'})]
13 [frozenset({'v7', 'v6'}), frozenset({'v2', 'v3'}), frozenset({'v1', 'v8'}),
14 frozenset({'v4', 'v5'})]
15 [frozenset({'v2', 'v1'}), frozenset({'v8', 'v7'}), frozenset({'v3', 'v4'}),
16 frozenset({'v5', 'v6'})]
17 number of solutions: 10
18 cardinality distribution
19 card.: [0, 1, 2, 3, 4, 5, 6, 7, 8]
20 freq.: [0, 0, 0, 8, 2, 0, 0, 0, 0]
21 execution time: 0.00029 sec.
```

The two last MISs of cardinality 4 (see Lines 13-16 above) give **isomorphic perfect maximum matchings** of the 8-cycle graph. Every vertex of the cycle is adjacent to a matching edge. Odd cycle graphs do not admit any perfect matching.

```
1>>> maxMatching = c8.computeMaximumMatching()
2>>> c8.exportGraphViz(fileName='maxMatchingcycleGraph',\
3... matching=maxMatching)
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to maxMatchingcyleGraph.dot
6 Matching: {frozenset({'v1', 'v2'}), frozenset({'v5', 'v6'}),
7 frozenset({'v3', 'v4'}), frozenset({'v7', 'v8'}) }
8 circo -Tpng maxMatchingcyleGraph.dot -o maxMatchingcyleGraph.png
```

### 1.16.5. Grids and the Ising model

Special classes of graphs, like *n* x *m* **rectangular** or **triangular grids** (`GridGraph`

and `IsingModel`

) are available in the `graphs`

module. For instance, we may use a Gibbs sampler again for simulating an **Ising Model** on such a grid.

```
1>>> from graphs import GridGraph, IsingModel
2>>> g = GridGraph(n=15,m=15)
3>>> g.showShort()
4 *----- show short --------------*
5 Grid graph : grid-6-6
6 n : 6
7 m : 6
8 order : 36
9>>> im = IsingModel(g,beta=0.3,nSim=100000,Debug=False)
10 Running a Gibbs Sampler for 100000 step !
11>>> im.exportGraphViz(colors=['lightblue','lightcoral'])
12 *---- exporting a dot file for GraphViz tools ---------*
13 Exporting to grid-15-15-ising.dot
14 fdp -Tpng grid-15-15-ising.dot -o grid-15-15-ising.png
```

### 1.16.6. Simulating Metropolis random walks

Finally, we provide the `MetropolisChain`

class, a specialization of the `Graph`

class, for implementing a generic **Metropolis MCMC** (Monte Carlo Markov Chain) sampler for simulating random walks on a given graph following a given probability *probs* = {‘v1’: x, ‘v2’: y, …} for visiting each vertex (see Lines 14-22).

```
1>>> from graphs import MetropolisChain
2>>> g = Graph(numberOfVertices=5,edgeProbability=0.5)
3>>> g.showShort()
4 *---- short description of the graph ----*
5 Name : 'randomGraph'
6 Vertices : ['v1', 'v2', 'v3', 'v4', 'v5']
7 Valuation domain : {'max': 1, 'med': 0, 'min': -1}
8 Gamma function :
9 v1 -> ['v2', 'v3', 'v4']
10 v2 -> ['v1', 'v4']
11 v3 -> ['v5', 'v1']
12 v4 -> ['v2', 'v5', 'v1']
13 v5 -> ['v3', 'v4']
```

```
1>>> probs = {} # initialize a potential stationary probability vector
2>>> n = g.order # for instance: probs[v_i] = n-i/Sum(1:n) for i in 1:n
3>>> i = 0
4>>> verticesList = [x for x in g.vertices]
5>>> verticesList.sort()
6>>> for v in verticesList:
7... probs[v] = (n - i)/(n*(n+1)/2)
8... i += 1
```

The `checkSampling()`

method (see Line 23) generates a random walk of *nSim=30000* steps on the given graph and records by the way the observed relative frequency with which each vertex is passed by.

```
1>>> met = MetropolisChain(g,probs)
2>>> frequency = met.checkSampling(verticesList[0],nSim=30000)
3>>> for v in verticesList:
4... print(v,probs[v],frequency[v])
5
6 v1 0.3333 0.3343
7 v2 0.2666 0.2680
8 v3 0.2 0.2030
9 v4 0.1333 0.1311
10 v5 0.0666 0.0635
```

In this example, the stationary transition probability distribution, shown by the `showTransitionMatrix()`

method above (see below), is quite adequately simulated.

```
1>>> met.showTransitionMatrix()
2 * ---- Transition Matrix -----
3 Pij | 'v1' 'v2' 'v3' 'v4' 'v5'
4 -----|-------------------------------------
5 'v1' | 0.23 0.33 0.30 0.13 0.00
6 'v2' | 0.42 0.42 0.00 0.17 0.00
7 'v3' | 0.50 0.00 0.33 0.00 0.17
8 'v4' | 0.33 0.33 0.00 0.08 0.25
9 'v5' | 0.00 0.00 0.50 0.50 0.00
```

For more technical information and more code examples, look into the technical documentation of the graphs module. For the readers interested in algorithmic applications of Markov Chains we may recommend consulting O. Häggström’s 2002 book: [FMCAA].

Back to Content Table

## 1.17. Computing the non isomorphic MISs of the 12-cycle graph

### 1.17.1. Introduction

Due to the public success of our common 2008 publication with Jean-Luc Marichal [ISOMIS-08] , we present in this tutorial an example Python session for computing the **non isomorphic maximal independent sets** (MISs) from the 12-cycle graph, i.e. a `CirculantDigraph`

class instance of order 12 and symmetric circulants 1 and -1.

```
1>>> from digraphs import CirculantDigraph
2>>> c12 = CirculantDigraph(order=12,circulants=[1,-1])
3>>> c12 # 12-cycle digraph instance
4 *------- Digraph instance description ------*
5 Instance class : CirculantDigraph
6 Instance name : c12
7 Digraph Order : 12
8 Digraph Size : 24
9 Valuation domain : [-1.0, 1.0]
10 Determinateness : 100.000
11 Attributes : ['name', 'order', 'circulants', 'actions',
12 'valuationdomain', 'relation', 'gamma',
13 'notGamma']
```

Such *n*-cycle graphs are also provided as undirected graph instances by the `CycleGraph`

class.

```
1>>> from graphs import CycleGraph
2>>> cg12 = CycleGraph(order=12)
3>>> cg12
4 *------- Graph instance description ------*
5 Instance class : CycleGraph
6 Instance name : cycleGraph
7 Graph Order : 12
8 Graph Size : 12
9 Valuation domain : [-1.0, 1.0]
10 Attributes : ['name', 'order', 'vertices', 'valuationDomain',
11 'edges', 'size', 'gamma']
12>>> cg12.exportGraphViz('cg12')
```

### 1.17.2. Computing the maximal independent sets (MISs)

A non isomorphic MIS corresponds in fact to a set of isomorphic MISs, i.e. an orbit of MISs under the automorphism group of the 12-cycle graph. We are now first computing all maximal independent sets that are detectable in the 12-cycle digraph with the `showMIS()`

method.

```
1>>> c12.showMIS(withListing=False)
2 *--- Maximal independent choices ---*
3 number of solutions: 29
4 cardinality distribution
5 card.: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
6 freq.: [0, 0, 0, 0, 3, 24, 2, 0, 0, 0, 0, 0, 0]
7 Results in c12.misset
```

In the 12-cycle graph, we observe 29 labelled MISs: – 3 of cardinality 4, 24 of cardinality 5, and 2 of cardinality 6. In case of n-cycle graphs with *n* > 20, as the cardinality of the MISs becomes big, it is preferable to use the shell *perrinMIS* command compiled from C and installed 3 along with all the Digraphs3 python modules for computing the set of MISs observed in the graph.

```
1 ...$ echo 12 | /usr/local/bin/perrinMIS
2 # -------------------------------------- #
3 # Generating MIS set of Cn with the #
4 # Perrin sequence algorithm. #
5 # Temporary files used. #
6 # even versus odd order optimised. #
7 # RB December 2006 #
8 # Current revision Dec 2018 #
9 # -------------------------------------- #
10 Input cycle order ? <-- 12
11 mis 1 : 100100100100
12 mis 2 : 010010010010
13 mis 3 : 001001001001
14 ...
15 ...
16 ...
17 mis 27 : 001001010101
18 mis 28 : 101010101010
19 mis 29 : 010101010101
20 Cardinalities:
21 0 : 0
22 1 : 0
23 2 : 0
24 3 : 0
25 4 : 3
26 5 : 24
27 6 : 2
28 7 : 0
29 8 : 0
30 9 : 0
31 10 : 0
32 11 : 0
33 12 : 0
34 Total: 29
35 execution time: 0 sec. and 2 millisec.
```

Reading in the result of the *perrinMIS* shell command, stored in a file called by default ‘curd.dat’, may be operated with the `readPerrinMisset()`

method.

```
1>>> c12.readPerrinMisset(file='curd.dat')
2>>> c12.misset
3 {frozenset({'5', '7', '10', '1', '3'}),
4 frozenset({'9', '11', '5', '2', '7'}),
5 frozenset({'7', '2', '4', '10', '12'}),
6 ...
7 ...
8 ...
9 frozenset({'8', '4', '10', '1', '6'}),
10 frozenset({'11', '4', '1', '9', '6'}),
11 frozenset({'8', '2', '4', '10', '12', '6'})
12 }
```

### 1.17.3. Computing the automorphism group

For computing the corresponding non isomorphic MISs, we actually need the automorphism group of the c12-cycle graph. The `Digraph`

class therefore provides the `automorphismGenerators()`

method which adds automorphism group generators to a `Digraph`

class instance with the help of the external shell *dreadnaut* command from the **nauty** software package 2.

```
1>>> c12.automorphismGenerators()
2
3 ...
4 Permutations
5 {'1': '1', '2': '12', '3': '11', '4': '10', '5':
6 '9', '6': '8', '7': '7', '8': '6', '9': '5', '10':
7 '4', '11': '3', '12': '2'}
8 {'1': '2', '2': '1', '3': '12', '4': '11', '5': '10',
9 '6': '9', '7': '8', '8': '7', '9': '6', '10': '5',
10 '11': '4', '12': '3'}
11>>> print('grpsize = ', c12.automorphismGroupSize)
12 grpsize = 24
```

The 12-cycle graph automorphism group is generated with both the permutations above and has group size 24.

### 1.17.4. Computing the isomorphic MISs

The command `showOrbits()`

renders now the labelled representatives of each of the four orbits of isomorphic MISs observed in the 12-cycle graph (see Lines 7-10).

```
1>>> c12.showOrbits(c12.misset,withListing=False)
2
3 ...
4 *---- Global result ----
5 Number of MIS: 29
6 Number of orbits : 4
7 Labelled representatives and cardinality:
8 1: ['2','4','6','8','10','12'], 2
9 2: ['2','5','8','11'], 3
10 3: ['2','4','6','9','11'], 12
11 4: ['1','4','7','9','11'], 12
12 Symmetry vector
13 stabilizer size: [1, 2, 3, ..., 8, 9, ..., 12, 13, ...]
14 frequency : [0, 2, 0, ..., 1, 0, ..., 1, 0, ...]
```

The corresponding group stabilizers’ sizes and frequencies – orbit 1 with 6 symmetry axes, orbit 2 with 4 symmetry axes, and orbits 3 and 4 both with one symmetry axis (see Lines 11-13), are illustrated in the corresponding unlabelled graphs of Fig. 1.61 below.

The non isomorphic MISs in the 12-cycle graph represent in fact all the ways one may write the number 12 as the circular sum of ‘2’s and ‘3’s without distinguishing opposite directions of writing. The first orbit corresponds to writing six times a ‘2’; the second orbit corresponds to writing four times a ‘3’. The third and fourth orbit correspond to writing two times a ‘3’ and three times a ‘2’. There are two non isomorphic ways to do this latter circular sum. Either separating the ‘3’s by one and two ‘2’s, or by zero and three ‘2’s (see Bisdorff & Marichal [ISOMIS-08] ).

Back to Content Table

## 1.18. On computing digraph kernels

### 1.18.1. What is a graph kernel ?

We call **choice** in a graph, respectively a digraph, a subset of its vertices, resp. of its nodes or actions. A choice *Y* is called **internally stable** or **independent** when there exist **no links** (edges) or relations (arcs) between its members. Furthermore, a choice *Y* is called **externally stable** when for each vertex, node or action *x* not in *Y*, there exists at least a member *y* of *Y* such that *x* is linked or related to *y*. Now, an internally **and** externally stable choice is called a **kernel**.

A first trivial example is immediately given by the maximal independent vertices sets (MISs) of the n-cycle graph (see tutorial on computing isomorphic choices). Indeed, each MIS in the n-cycle graph is by definition independent, i.e. internally stable, and each non selected vertex in the n-cycle graph is in relation with either one or even two members of the MIS. See, for instance, the four non isomorphic MISs of the 12-cycle graph as shown in Fig. 1.61.

In all graph or symmetric digraph, the *maximality condition* imposed on the internal stability is equivalent to the external stability condition. Indeed, if there would exist a vertex or node not related to any of the elements of a choice, then we may safely add this vertex or node to the given choice without violating its internal stability. All kernels must hence be maximal independent choices. In fact, in a topological sense, they correspond to maximal **holes** in the given graph.

We may illustrate this coincidence between MISs and kernels in graphs and symmetric digraphs with the following random 3-regular graph instance (see Fig. 1.62).

```
1>>> from graphs import RandomRegularGraph
2>>> g = RandomRegularGraph(order=12,degree=3,seed=100)
3>>> g.exportGraphViz('random3RegularGraph')
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to random3RegularGraph.dot
6 fdp -Tpng random3RegularGraph.dot -o random3RegularGraph.png
```

A random MIS in this graph may be computed for instance by using the `MISModel`

class.

```
1>>> from graphs import MISModel
2>>> mg = MISModel(g)
3 Iteration: 1
4 Running a Gibbs Sampler for 660 step !
5 {'a06', 'a02', 'a12', 'a10'} is maximal !
6>>> mg.exportGraphViz('random3RegularGraph_mis')
7 *---- exporting a dot file for GraphViz tools ---------*
8 Exporting to random3RegularGraph-mis.dot
9 fdp -Tpng random3RegularGraph-mis.dot -o random3RegularGraph-mis.png
```

It is easily verified in Fig. 1.63 above, that the computed MIS renders indeed a valid kernel of the given graph. The complete set of kernels of this 3-regular graph instance coincides hence with the set of its MISs.

```
1>>> g.showMIS()
2 *--- Maximal Independent Sets ---*
3 ['a01', 'a02', 'a03', 'a07']
4 ['a01', 'a04', 'a05', 'a08']
5 ['a04', 'a05', 'a08', 'a09']
6 ['a01', 'a04', 'a05', 'a10']
7 ['a04', 'a05', 'a09', 'a10']
8 ['a02', 'a03', 'a07', 'a12']
9 ['a01', 'a03', 'a07', 'a11']
10 ['a05', 'a08', 'a09', 'a11']
11 ['a03', 'a07', 'a11', 'a12']
12 ['a07', 'a09', 'a11', 'a12']
13 ['a08', 'a09', 'a11', 'a12']
14 ['a04', 'a05', 'a06', 'a08']
15 ['a04', 'a05', 'a06', 'a10']
16 ['a02', 'a04', 'a06', 'a10']
17 ['a02', 'a03', 'a06', 'a12']
18 ['a02', 'a06', 'a10', 'a12']
19 ['a01', 'a02', 'a04', 'a07', 'a10']
20 ['a02', 'a04', 'a07', 'a09', 'a10']
21 ['a02', 'a07', 'a09', 'a10', 'a12']
22 ['a01', 'a03', 'a05', 'a08', 'a11']
23 ['a03', 'a05', 'a06', 'a08', 'a11']
24 ['a03', 'a06', 'a08', 'a11', 'a12']
25 number of solutions: 22
26 cardinality distribution
27 card.: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
28 freq.: [0, 0, 0, 0, 16, 6, 0, 0, 0, 0, 0, 0, 0]
29 execution time: 0.00045 sec.
30 Results in self.misset
31>>> g.misset
32 [frozenset({'a02', 'a01', 'a07', 'a03'}),
33 frozenset({'a04', 'a01', 'a08', 'a05'}),
34 frozenset({'a09', 'a04', 'a08', 'a05'}),
35 ...
36 ...
37 frozenset({'a06', 'a02', 'a12', 'a10'}),
38 frozenset({'a06', 'a11', 'a08', 'a03', 'a05'}),
39 frozenset({'a03', 'a06', 'a11', 'a12', 'a08'})]
```

We cannot resist in looking in this 3-regular graph for non isomorphic kernels (MISs, see previous tutorial). To do so we must first, convert the given *graph* instance into a *digraph* instance. Then, compute its automorphism generators, and finally, identify the isomorphic kernel orbits.

```
1>>> dg = g.graph2Digraph()
2>>> dg.showMIS()
3 *--- Maximal independent choices ---*
4 ...
5 ['a06', 'a02', 'a12', 'a10']
6 ...
7 number of solutions: 22
8 cardinality distribution
9 card.: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
10 freq.: [0, 0, 0, 0, 16, 6, 0, 0, 0, 0, 0, 0, 0]
11 execution time: 0.00080 sec.
12 Results in self.misset
13>>> dg.automorphismGenerators()
14 *----- saving digraph in nauty dre format -------------*
15 ...
16 # automorphisms extraction from dre file #
17 # Using input file: randomRegularGraph.dre
18 echo '<randomRegularGraph.dre -m p >randomRegularGraph.auto x' | dreadnaut
19 # permutation = 1['1', '11', '7', '5', '4', '9', '3', '10', '6', '8', '2', '12']
20>>> dg.showOrbits(dg.misset)
21 *--- Isomorphic reduction of choices
22 ...
23 current representative: frozenset({'a09', 'a11', 'a12', 'a08'})
24 length : 4
25 number of isomorph choices 2
26 isormorph choices
27 ['a06', 'a02', 'a12', 'a10'] # <<== the random MIS shown above
28 ['a09', 'a11', 'a12', 'a08']
29 ...
30 *---- Global result ----
31 Number of choices: 22
32 Number of orbits : 11
33 Labelled representatives:
34 ...
35 ['a09', 'a11', 'a12', 'a08']
36 ...
```

In our random 3-regular graph instance (see Fig. 1.62), we may thus find eleven non isomorphic kernels with orbit sizes equal to two. We illustrate below the isomorphic twin of the random MIS example shown in Fig. 1.63 .

All graphs and symmetric digraphs admit MISs, hence also kernels.

It is worthwhile noticing that the **maximal matchings** of a graph correspond bijectively to its line graph’s **kernels** (see the `LineGraph`

class).

```
1>>> from graphs import CycleGraph
2>>> c8 = CycleGraph(order=8)
3>>> maxMatching = c8.computeMaximumMatching()
4>>> c8.exportGraphViz(fileName='maxMatchingcycleGraph',\
5... matching=maxMatching)
6 *---- exporting a dot file for GraphViz tools ---------*
7 Exporting to maxMatchingcyleGraph.dot
8 Matching: {frozenset({'v1', 'v2'}), frozenset({'v5', 'v6'}),
9 frozenset({'v3', 'v4'}), frozenset({'v7', 'v8'}) }
10 circo -Tpng maxMatchingcyleGraph.dot -o maxMatchingcyleGraph.png
```

In the context of digraphs, i.e. *oriented* graphs, the kernel concept gets much richer and separates from the symmetric MIS concept.

### 1.18.2. Initial and terminal kernels

In an oriented graph context, the internal stability condition of the kernel concept remains untouched; however, the external stability condition gets indeed split up by the *orientation* into two lateral cases:

A

dominantstability condition, where each non selected node isdominatedby at least one member of the kernel;An

absorbentstability condition, where each non selected node isabsorbedby at least one member of the kernel.

A both *internally* **and** *dominant*, resp. *absorbent stable* choice is called a *dominant* or **initial**, resp. an *absorbent* or **terminal** kernel. From a topological perspective, the initial kernel concept looks from the outside of the digraph into its interior, whereas the terminal kernel looks from the interior of a digraph toward its outside. From an algebraic perspective, the initial kernel is a *prefix* operand, and the terminal kernel is a *postfix* operand in the kernel equation systems (see Digraph3 advanced topic on bipolar-valued kernel membership characteristics).

Furthermore, as the kernel concept involves conjointly a **positive logical refutation** (the *internal stability*) and a **positive logical affirmation** (the *external stability*), it appeared rather quickly necessary in our operational developments to adopt a bipolar characteristic [-1,1] valuation domain, modelling *negation* by change of numerical sign and including explicitly a third **median** logical value (0) expressing logical **indeterminateness** (neither positive, nor negative, see [BIS-2000] and [BIS-2004a]).

In such a bipolar-valued context, we call **prekernel** a choice which is **externally stable** and for which the **internal stability** condition is **valid or indeterminate**. We say that the independence condition is in this case only **weakly** validated. Notice that all kernels are hence prekernels, but not vice-versa.

In graphs or symmetric digraphs, where there is essentially no apparent ‘ *laterality* ‘, all prekernels are *initial* **and** *terminal* at the same time. They correspond to what we call *holes* in the graph. A *universal* example is given by the **complete** digraph.

```
1>>> from digraphs import CompleteDigraph
2>>> u = CompleteDigraph(order=5)
3>>> u
4 *------- Digraph instance description ------*
5 Instance class : CompleteDigraph
6 Instance name : complete
7 Digraph Order : 5
8 Digraph Size : 20
9 Valuation domain : [-1.00 ; 1.00]
10 ---------------------------------
11>>> u.showPreKernels()
12 *--- Computing preKernels ---*
13 Dominant kernels :
14 ['1'] independence: 1.0; dominance : 1.0; absorbency : 1.0
15 ['2'] independence: 1.0; dominance : 1.0; absorbency : 1.0
16 ['3'] independence: 1.0; dominance : 1.0; absorbency : 1.0
17 ['4'] independence: 1.0; dominance : 1.0; absorbency : 1.0
18 ['5'] independence: 1.0; dominance : 1.0; absorbency : 1.0
19 Absorbent kernels :
20 ['1'] independence: 1.0; dominance : 1.0; absorbency : 1.0
21 ['2'] independence: 1.0; dominance : 1.0; absorbency : 1.0
22 ['3'] independence: 1.0; dominance : 1.0; absorbency : 1.0
23 ['4'] independence: 1.0; dominance : 1.0; absorbency : 1.0
24 ['5'] independence: 1.0; dominance : 1.0; absorbency : 1.0
25 *----- statistics -----
26 graph name: complete
27 number of solutions
28 dominant kernels : 5
29 absorbent kernels: 5
30 cardinality frequency distributions
31 cardinality : [0, 1, 2, 3, 4, 5]
32 dominant kernel : [0, 5, 0, 0, 0, 0]
33 absorbent kernel: [0, 5, 0, 0, 0, 0]
34 Execution time : 0.00004 sec.
35 Results in sets: dompreKernels and abspreKernels.
```

In a complete digraph, each single node is indeed both an initial and a terminal prekernel candidate and there is no definite *begin* or *end* of the digraph to be detected. *Laterality* is here entirely *relative* to a specific singleton chosen as reference point of view. The same absence of laterality is apparent in two other universal digraph models, the **empty** and the **indeterminate** digraph.

```
1>>> ed = EmptyDigraph(order=5)
2>>> ed.showPreKernels()
3 *--- Computing preKernels ---*
4 Dominant kernel :
5 ['1', '2', '3', '4', '5']
6 independence : 1.0
7 dominance : 1.0
8 absorbency : 1.0
9 Absorbent kernel :
10 ['1', '2', '3', '4', '5']
11 independence : 1.0
12 dominance : 1.0
13 absorbency : 1.0
14 ...
```

In the empty digraph, the whole set of nodes gives indeed at the same time the **unique** *initial* **and** *terminal* prekernel. Similarly, for the **indeterminate** digraph.

```
1>>> from digraphs import IndeterminateDigraph
2>>> id = IndeterminateDigraph(order=5)
3>>> id.showPreKernels()
4 *--- Computing preKernels ---*
5 Dominant prekernel :
6 ['1', '2', '3', '4', '5']
7 independence : 0.0 # <<== indeterminate
8 dominance : 1.0
9 absorbency : 1.0
10 Absorbent prekernel :
11 ['1', '2', '3', '4', '5']
12 independence : 0.0 # <<== indeterminate
13 dominance : 1.0
14 absorbency : 1.0
```

Both these results make sense, as in a completely empty or indeterminate digraph, there is no *interior* of the digraph defined, only a *border* which is hence at the same time an initial and terminal prekernel. Notice however, that in the latter indeterminate case, the complete set of nodes verifies only weakly the internal stability condition (see above).

Other common digraph models, although being clearly oriented, may show nevertheless no apparent laterality, like **odd chordless circuits**, i.e. *holes* surrounded by an *oriented cycle* -a circuit- of odd length. They do not admit in fact any initial or terminal prekernel.

```
1>>> from digraphs import CirculantDigraph
2>>> c5 = CirculantDigraph(order=5,circulants=[1])
3>>> c5.showPreKernels()
4 *----- statistics -----
5 digraph name: c5
6 number of solutions
7 dominant prekernels : 0
8 absorbent prekernels: 0
```

Chordless circuits of **even** length 2 x *k*, with *k* > 1, contain however two isomorphic prekernels of cardinality *k* which qualify conjointly as initial and terminal candidates.

```
1>>> c6 = CirculantDigraph(order=6,circulants=[1])
2>>> c6.showPreKernels()
3 *--- Computing preKernels ---*
4 Dominant preKernels :
5 ['1', '3', '5'] independence: 1.0, dominance: 1.0, absorbency: 1.0
6 ['2', '4', '6'] independence: 1.0, dominance: 1.0, absorbency: 1.0
7 Absorbent preKernels :
8 ['1', '3', '5'] independence: 1.0, dominance: 1.0, absorbency: 1.0
9 ['2', '4', '6'] independence: 1.0, dominance: 1.0, absorbency: 1.0
```

Chordless circuits of even length may thus be indifferently oriented along two opposite directions. Notice by the way that the duals of **all** chordless circuits of *odd* **or** *even* length, i.e. *filled* circuits also called **anti-holes** (see Fig. 1.66), never contain any potential prekernel candidates.

```
1>>> dc6 = -c6 # dc6 = DualDigraph(c6)
2>>> dc6.showPreKernels()
3 *----- statistics -----
4 graph name: dual_c6
5 number of solutions
6 dominant prekernels : 0
7 absorbent prekernels: 0
8>>> dc6.exportGraphViz(fileName='dualChordlessCircuit')
9 *---- exporting a dot file for GraphViz tools ---------*
10 Exporting to dualChordlessCircuit.dot
11 circo -Tpng dualChordlessCircuit.dot -o dualChordlessCircuit.png
```

We call **weak**, a *chordless circuit* with *indeterminate inner part*. The `CirculantDigraph`

class provides a parameter for constructing such a kind of *weak chordless* circuits.

```
1>>> c6 = CirculantDigraph(order=6, circulants=[1],\
2... IndeterminateInnerPart=True)
```

It is worth noticing that the *dual* version (14) of a *weak* circuit corresponds to its *converse* version, i.e. *-c6* = *~c6* (see Fig. 1.67).

```
1>>> (-c6).exportGraphViz()
2 *---- exporting a dot file for GraphViz tools ---------*
3 Exporting to dual_c6.dot
4 circo -Tpng dual_c6.dot -o dual_c6.png
5>>> (~c6).exportGraphViz()
6 *---- exporting a dot file for GraphViz tools ---------*
7 Exporting to converse_c6.dot
8 circo -Tpng converse_c6.dot -o converse_c6.png
```

It immediately follows that weak chordless circuits are part of the class of digraphs that are **invariant** under the *codual* transform, *cn* = - (~ *cn* ) = ~ ( -*cn* ) 13.

### 1.18.3. Kernels in lateralized digraphs

Humans do live in an apparent physical space of plain transitive **lateral orientation**, fully empowered in finite geometrical 3D models with **linear orders**, where first, resp. last ranked, nodes deliver unique initial, resp. terminal, kernels. Similarly, in finite **preorders**, the first, resp. last, equivalence classes deliver the unique initial, resp. unique terminal, kernels. More generally, in finite **partial orders**, i.e. asymmetric and transitive digraphs, topological sort algorithms will easily reveal on the first, resp. last, level all unique initial, resp. terminal, kernels.

In genuine random digraphs, however, we may need to check for each of its MISs, whether *one*, *both*, or *none* of the lateralized external stability conditions may be satisfied. Consider, for instance, the following random digraph instance of order 7 and generated with an arc probability of 30%.

```
1>>> from randomDigraphs import RandomDigraph
2>>> rd = RandomDigraph(order=7,arcProbability=0.3,seed=5)
3>>> rd.exportGraphViz('randomLaterality')
4 *---- exporting a dot file for GraphViz tools ---------*
5 Exporting to randomLaterality.dot
6 dot -Grankdir=BT -Tpng randomLaterality.dot -o randomLaterality.png
```

The random digraph shown in Fig. 1.68 above has no apparent special properties, except from being connected (see Line 3 below).

```
1>>> rd.showComponents()
2 *--- Connected Components ---*
3 1: ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7']
4>>> rd.computeSymmetryDegree(Comments=True,InPercents=True)
5 Symmetry degree (%) of digraph <randomDigraph>:
6 #arcs x>y: 14, #symmetric: 1, #asymmetric: 13
7 #symmetric/#arcs = 7.1
8>>> rd.computeChordlessCircuits()
9 [] # no chordless circuits detected
10>>> rd.computeTransitivityDegree(Comments=True,InPercents=True)
11 Transitivity degree (%) of graph <randomDigraph>:
12 #triples x>y>z: 23, #closed: 11, #open: 12
13 #closed/#triples = 47.8
```

The given digraph instance is neither asymmetric (a3 <–> a6) nor symmetric (a2 –> a1, a1 -/> a2) (see Line 6 above); there are no chordless circuits (see Line 9 above); and, the digraph is not transitive (a5 -> a2 -> a1, but a5 -/> a1). More than half of the required transitive closure is missing (see Line 12 above).

Now, we know that its potential prekernels must be among its set of maximal independent choices.

```
1>>> rd.showMIS()
2 *--- Maximal independent choices ---*
3 ['a2', 'a4', 'a6']
4 ['a6', 'a1']
5 ['a5', 'a1']
6 ['a3', 'a1']
7 ['a4', 'a3']
8 ['a7']
9 ------
10>>> rd.showPreKernels()
11 *--- Computing preKernels ---*
12 Dominant preKernels :
13 ['a2', 'a4', 'a6']
14 independence : 1.0
15 dominance : 1.0
16 absorbency : -1.0
17 covering : 0.500
18 ['a4', 'a3']
19 independence : 1.0
20 dominance : 1.0
21 absorbency : -1.0
22 covering : 0.600 # <<==
23 Absorbent preKernels :
24 ['a3', 'a1']
25 independence : 1.0
26 dominance : -1.0
27 absorbency : 1.0
28 covering : 0.500
29 ['a6', 'a1']
30 independence : 1.0
31 dominance : -1.0
32 absorbency : 1.0
33 covering : 0.600 # <<==
34 ...
```

Among the six MISs contained in this random digraph (see above Lines 3-8) we discover two initial and two terminal kernels (Lines 12-34). Notice by the way the covering values (between 0.0 and 1.0) shown by the `digraphs.Digraph.showPreKernels()`

method (Lines 17, 22, 28 and 33). The higher this value, the more the corresponding kernel candidate makes apparent the digraph’s *laterality*. We may hence redraw the same digraph in Fig. 1.69 by looking into its interior via the *best covering* initial kernel candidate: the dominant choice {‘a3’,’4a’} (coloured in yellow), and looking out of it via the *best covered* terminal kernel candidate: the absorbent choice {‘a1’,’a6’} (coloured in blue).

```
1>>> rd.exportGraphViz(fileName='orientedLaterality',\
2... bestChoice=set(['a3', 'a4']),\
3... worstChoice=set(['a1', 'a6']))
4*---- exporting a dot file for GraphViz tools ---------*
5 Exporting to orientedLaterality.dot
6 dot -Grankdir=BT -Tpng orientedLaterality.dot -o orientedLaterality.png
```

In algorithmic decision theory, initial and terminal prekernels may provide convincing best, resp. worst, choice recommendations (see tutorial on computing a best choice recommendation).

### 1.18.4. Computing good and bad choice recommendations

To illustrate this idea, let us finally compute good and bad choice recommendations in the following random bipolar-valued **outranking** digraph.

```
1>>> from outrankingDigraphs import *
2>>> g = RandomBipolarOutrankingDigraph(seed=5)
3>>> g
4 *------- Object instance description ------*
5 Instance class : RandomBipolarOutrankingDigraph
6 Instance name : randomOutranking
7 # Actions : 7
8 # Criteria : 7
9 Size : 26
10 Determinateness : 34.275
11 Valuation domain : {'min': -100.0, 'med': 0.0, 'max': 100.0}
12>>> g.showHTMLPerformanceTableau()
```

The underlying random performance tableau (see Fig. 1.70) shows the performance grading of 7 potential decision actions with respect to 7 decision criteria supporting each an increasing performance scale from 0 to 100. Notice the missing performance data concerning decision actions ‘a2’ and ‘a5’. The resulting **strict outranking** - i.e. a weighted majority supported - *better than without considerable counter-performance* - digraph is shown in Fig. 1.71 below.

```
1>>> gcd = ~(-g) # Codual: the converse of the negation
2>>> gcd.exportGraphViz(fileName='tutOutRanking')
3 *---- exporting a dot file for GraphViz tools ---------*
4 Exporting to tutOutranking.dot
5 dot -Grankdir=BT -Tpng tutOutranking.dot -o tutOutranking.png
```

All decision actions appear strictly better performing than action ‘a7’. We call it a **Condorcet loser** and it is an evident terminal prekernel candidate. On the other side, three actions: ‘a1’, ‘a2’ and ‘a4’ are not dominated. They give together an initial prekernel candidate.

```
1>>> gcd.showPreKernels()
2 *--- Computing preKernels ---*
3 Dominant preKernels :
4 ['a1', 'a2', 'a4']
5 independence : 0.00
6 dominance : 6.98
7 absorbency : -48.84
8 covering : 0.667
9 Absorbent preKernels :
10 ['a3', 'a7']
11 independence : 0.00
12 dominance : -74.42
13 absorbency : 16.28
14 covered : 0.800
```

With such unique disjoint initial and terminal prekernels (see Line 4 and 10), the given digraph instance is hence clearly *lateralized*. Indeed, these initial and terminal prekernels of the codual outranking digraph reveal best, resp. worst, choice recommendations one may formulate on the basis of a given outranking digraph instance.

```
1>>> g.showBestChoiceRecommendation()
2 ***********************
3 Rubis best choice recommendation(s) (BCR)
4 (in decreasing order of determinateness)
5 Credibility domain: [-100.00,100.00]
6 === >> potential first choice(s)
7 * choice : ['a1', 'a2', 'a4']
8 independence : 0.00
9 dominance : 6.98
10 absorbency : -48.84
11 covering (%) : 66.67
12 determinateness (%) : 57.97
13 - most credible action(s) = { 'a4': 20.93, 'a2': 20.93, }
14 === >> potential last choice(s)
15 * choice : ['a3', 'a7']
16 independence : 0.00
17 dominance : -74.42
18 absorbency : 16.28
19 covered (%) : 80.00
20 determinateness (%) : 64.62
21 - most credible action(s) = { 'a7': 48.84, }
```

Notice that solving bipolar-valued kernel equation systems (see Bipolar-Valued Kernels in the Advanced Topics) provides furthermore a positive characterization of the most credible decision actions in each respective choice recommendation (see Lines 14 and 23 above). Actions ‘a2’ and ‘a4’ are equivalent candidates for a unique best choice, and action ‘a7’ is clearly confirmed as the last choice.

In Fig. 1.72 below, we orient the drawing of the strict outranking digraph instance with the help of these first and last choice recommendations.