Edge Ordering
Edge ordering makes a difference on the on the join tree and potentials produced. Let’s take the BBN network structure below where all nodes are binary having the values on
and off
.
a –> c <– b
Note how c
has 2 parents, a
and b
. The potential (or conditional probability table CPT
) for c
is specified as a list of probabilities as follows.
[0.7, 0.3, 0.2, 0.8, 0.6, 0.4, 0.6, 0.4]
Let’s say that this list of probabilities represents the CPT below.
| | | c=on | c=off |
|-------|-------|------|-------|
| a=on | b=on | 0.7 | 0.3 |
| a=on | b=off | 0.2 | 0.8 |
| a=off | b=on | 0.6 | 0.4 |
| a=off | b=off | 0.6 | 0.4 |
When we define a BBN structure (be it programmatically in code/Python or declaratively in JSON), we should define and add the edge a -> c
to the graph before the edge b -> c
. Below is the code where we do so.
[1]:
from pybbn.graph.dag import Bbn
from pybbn.graph.edge import Edge, EdgeType
from pybbn.graph.node import BbnNode
from pybbn.graph.variable import Variable
def get_bbn1():
a = BbnNode(Variable(0, 'a', ['on', 'off']), [0.2, 0.8])
b = BbnNode(Variable(1, 'b', ['on', 'off']), [0.8, 0.2])
c = BbnNode(Variable(2, 'c', ['on', 'off']), [0.7, 0.3, 0.2, 0.8, 0.6, 0.4, 0.6, 0.4])
bbn = Bbn() \
.add_node(a) \
.add_node(b) \
.add_node(c) \
.add_edge(Edge(a, c, EdgeType.DIRECTED)) \
.add_edge(Edge(b, c, EdgeType.DIRECTED))
return bbn
When we add the edge b -> c
to the network structure before a -> c
, then the induced CPT for c
will be as follows. This second CPT for c
is not the same at all for the first one!
| | | c=on | c=off |
|-------|-------|------|-------|
| b=on | a=on | 0.7 | 0.3 |
| b=on | a=off | 0.2 | 0.8 |
| b=off | a=on | 0.6 | 0.4 |
| b=off | a=off | 0.6 | 0.4 |
Here is the code for creating a BBN where we add b -> c
before a -> c
.
[2]:
def get_bbn2():
a = BbnNode(Variable(0, 'a', ['on', 'off']), [0.2, 0.8])
b = BbnNode(Variable(1, 'b', ['on', 'off']), [0.8, 0.2])
c = BbnNode(Variable(2, 'c', ['on', 'off']), [0.7, 0.3, 0.2, 0.8, 0.6, 0.4, 0.6, 0.4])
bbn = Bbn() \
.add_node(a) \
.add_node(b) \
.add_node(c) \
.add_edge(Edge(b, c, EdgeType.DIRECTED)) \
.add_edge(Edge(a, c, EdgeType.DIRECTED))
return bbn
Although the networks (regardless of the order of how we add the edges) are the same in both cases, the parameters induced are NOT and sensitive to the order of how the edges are added. Now, let’s compare the posteriors of of these 2 BBNs.
[3]:
from pybbn.pptc.inferencecontroller import InferenceController
b1 = get_bbn1()
b2 = get_bbn2()
j1 = InferenceController.apply(b1)
j2 = InferenceController.apply(b2)
Here are the posteriors for the first BBN. Note that the id-to-name as defined above are as follows.
0: a
1: b
2: c
Keep an eye on id 2, thus.
[4]:
for node in j1.get_bbn_nodes():
potential = j1.get_bbn_potential(node)
print(potential)
print('-' * 10)
1=on|0.80000
1=off|0.20000
----------
2=on|0.60000
2=off|0.40000
----------
0=on|0.20000
0=off|0.80000
----------
Here are the posteriors for the second BBN.
[5]:
for node in j2.get_bbn_nodes():
potential = j2.get_bbn_potential(node)
print(potential)
print('-' * 10)
1=on|0.80000
1=off|0.20000
----------
2=on|0.36000
2=off|0.64000
----------
0=on|0.20000
0=off|0.80000
----------
For now, there is no workaround for this issue of logically identical specified BBNs producing different potentials as a result of edge insertion order. Just make sure you are aware and careful.
Simple Example
Let’s say you have a DAG with 5 variables: \(X_1, X_2, X_3, X_4, X_5\) and the structure represented by an edge list is as follows.
\(X_1 \rightarrow X_5\)
\(X_2 \rightarrow X_5\)
\(X_3 \rightarrow X_5\)
\(X_4 \rightarrow X_5\)
The domains (or number of values) for each variable is as follows.
\(X_1 \in \{v_1, v_2\}\)
\(X_2 \in \{v_1, v_2, v_3\}\)
\(X_3 \in \{v_1, v_2\}\)
\(X_4 \in \{v_1, v_2, v_3, v_4, v_5\}\)
\(X_5 \in \{v_1, v_2, v_3\}\)
The question is, how do we build the parameters for \(X_5\)?
Let’s create some fake data.
[63]:
import pandas as pd
import numpy as np
import random
np.random.seed(37)
random.seed(37)
def get_data(n_values, n_samples):
return [f'v{v}' for v in np.random.randint(0, n_values, n_samples)]
N = 1_000
df = pd.DataFrame({
'x1': get_data(2, N),
'x2': get_data(3, N),
'x3': get_data(2, N),
'x4': get_data(5, N),
'x5': get_data(3, N)
})
df.shape
[63]:
(1000, 5)
[64]:
df.head()
[64]:
x1 | x2 | x3 | x4 | x5 | |
---|---|---|---|---|---|
0 | v1 | v2 | v0 | v4 | v2 |
1 | v1 | v0 | v1 | v3 | v0 |
2 | v0 | v1 | v1 | v2 | v2 |
3 | v1 | v2 | v0 | v1 | v2 |
4 | v0 | v2 | v1 | v1 | v2 |
Now, let’s verify the domains.
[65]:
def get_domains(df):
return {c: sorted(list(df[c].unique())) for c in df.columns}
domains = get_domains(df)
domains
[65]:
{'x1': ['v0', 'v1'],
'x2': ['v0', 'v1', 'v2'],
'x3': ['v0', 'v1'],
'x4': ['v0', 'v1', 'v2', 'v3', 'v4'],
'x5': ['v0', 'v1', 'v2']}
You want to create a conditional probability table CPT for \(X_5\) that looks like the following. Note that we simply show you the CPT shape and not the actual values. We will go into computing the actual value in a bit.
[66]:
import itertools
pas = [c for c in domains if c != 'x5']
cpt = (domains[pa] for pa in pas)
cpt = itertools.product(*cpt)
cpt = pd.DataFrame(cpt, columns=pas) \
.assign(**{v: 0.0 for v in domains['x5']}) \
.set_index(pas)
cpt
[66]:
v0 | v1 | v2 | ||||
---|---|---|---|---|---|---|
x1 | x2 | x3 | x4 | |||
v0 | v0 | v0 | v0 | 0.0 | 0.0 | 0.0 |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | v0 | 0.0 | 0.0 | 0.0 | |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v2 | v0 | v0 | 0.0 | 0.0 | 0.0 | |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | v0 | v0 | 0.0 | 0.0 | 0.0 |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | v0 | 0.0 | 0.0 | 0.0 | |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v2 | v0 | v0 | 0.0 | 0.0 | 0.0 | |
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 | |||
v1 | v0 | 0.0 | 0.0 | 0.0 | ||
v1 | 0.0 | 0.0 | 0.0 | |||
v2 | 0.0 | 0.0 | 0.0 | |||
v3 | 0.0 | 0.0 | 0.0 | |||
v4 | 0.0 | 0.0 | 0.0 |
Ok, so how do we create the CPT for \(X_5\) given the data? Here’s some code to demonstrate how to build the CPT.
[68]:
def get_cond_probs(q, d, pas, ch, df):
def save_divide(num, den):
try:
return num / den
except:
return 0.0
p_b = df.query(q).shape[0]
cp_pa = {f'x{i+1}': p for i, p in enumerate(pas)}
cp_ch = {v: save_divide(df.query(f'{q} and {ch}=="{v}"').shape[0], p_b) for v in d}
cp = {**cp_pa, **cp_ch}
return cp
pa_values = list(itertools.product(*(domains[pa] for pa in pas)))
queries = map(lambda tup: [f'x{i+1}=="{v}"' for i, v in enumerate(tup)], pa_values)
queries = map(lambda arr: ' and '.join(arr), queries)
queries = list(queries)
cpt = pd.DataFrame((get_cond_probs(q, domains['x5'], pas, 'x5', df) for q, pas in zip(queries, pa_values))) \
.set_index(['x1', 'x2', 'x3', 'x4'])
cpt
[68]:
v0 | v1 | v2 | ||||
---|---|---|---|---|---|---|
x1 | x2 | x3 | x4 | |||
v0 | v0 | v0 | v0 | 0.466667 | 0.266667 | 0.266667 |
v1 | 0.285714 | 0.428571 | 0.285714 | |||
v2 | 0.583333 | 0.166667 | 0.250000 | |||
v3 | 0.538462 | 0.307692 | 0.153846 | |||
v4 | 0.400000 | 0.000000 | 0.600000 | |||
v1 | v0 | 0.533333 | 0.333333 | 0.133333 | ||
v1 | 0.411765 | 0.117647 | 0.470588 | |||
v2 | 0.357143 | 0.357143 | 0.285714 | |||
v3 | 0.434783 | 0.173913 | 0.391304 | |||
v4 | 0.368421 | 0.421053 | 0.210526 | |||
v1 | v0 | v0 | 0.375000 | 0.375000 | 0.250000 | |
v1 | 0.266667 | 0.400000 | 0.333333 | |||
v2 | 0.312500 | 0.250000 | 0.437500 | |||
v3 | 0.400000 | 0.200000 | 0.400000 | |||
v4 | 0.190476 | 0.428571 | 0.380952 | |||
v1 | v0 | 0.157895 | 0.368421 | 0.473684 | ||
v1 | 0.307692 | 0.384615 | 0.307692 | |||
v2 | 0.388889 | 0.333333 | 0.277778 | |||
v3 | 0.250000 | 0.375000 | 0.375000 | |||
v4 | 0.200000 | 0.250000 | 0.550000 | |||
v2 | v0 | v0 | 0.333333 | 0.166667 | 0.500000 | |
v1 | 0.142857 | 0.428571 | 0.428571 | |||
v2 | 0.320000 | 0.440000 | 0.240000 | |||
v3 | 0.285714 | 0.428571 | 0.285714 | |||
v4 | 0.500000 | 0.250000 | 0.250000 | |||
v1 | v0 | 0.476190 | 0.333333 | 0.190476 | ||
v1 | 0.333333 | 0.333333 | 0.333333 | |||
v2 | 0.307692 | 0.076923 | 0.615385 | |||
v3 | 0.312500 | 0.375000 | 0.312500 | |||
v4 | 0.562500 | 0.187500 | 0.250000 | |||
v1 | v0 | v0 | v0 | 0.333333 | 0.285714 | 0.380952 |
v1 | 0.222222 | 0.388889 | 0.388889 | |||
v2 | 0.235294 | 0.411765 | 0.352941 | |||
v3 | 0.333333 | 0.333333 | 0.333333 | |||
v4 | 0.470588 | 0.235294 | 0.294118 | |||
v1 | v0 | 0.368421 | 0.315789 | 0.315789 | ||
v1 | 0.461538 | 0.192308 | 0.346154 | |||
v2 | 0.263158 | 0.315789 | 0.421053 | |||
v3 | 0.411765 | 0.235294 | 0.352941 | |||
v4 | 0.315789 | 0.368421 | 0.315789 | |||
v1 | v0 | v0 | 0.304348 | 0.260870 | 0.434783 | |
v1 | 0.454545 | 0.454545 | 0.090909 | |||
v2 | 0.300000 | 0.350000 | 0.350000 | |||
v3 | 0.454545 | 0.181818 | 0.363636 | |||
v4 | 0.214286 | 0.428571 | 0.357143 | |||
v1 | v0 | 0.285714 | 0.357143 | 0.357143 | ||
v1 | 0.235294 | 0.411765 | 0.352941 | |||
v2 | 0.315789 | 0.315789 | 0.368421 | |||
v3 | 0.363636 | 0.227273 | 0.409091 | |||
v4 | 0.222222 | 0.333333 | 0.444444 | |||
v2 | v0 | v0 | 0.272727 | 0.181818 | 0.545455 | |
v1 | 0.190476 | 0.523810 | 0.285714 | |||
v2 | 0.391304 | 0.260870 | 0.347826 | |||
v3 | 0.428571 | 0.214286 | 0.357143 | |||
v4 | 0.333333 | 0.166667 | 0.500000 | |||
v1 | v0 | 0.235294 | 0.352941 | 0.411765 | ||
v1 | 0.166667 | 0.500000 | 0.333333 | |||
v2 | 0.300000 | 0.200000 | 0.500000 | |||
v3 | 0.263158 | 0.368421 | 0.368421 | |||
v4 | 0.400000 | 0.333333 | 0.266667 |
Finally, we can flatten the CPT as follows.
[71]:
cpt_x5 = np.ravel(cpt).tolist()
cpt_x5
[71]:
[0.4666666666666667,
0.26666666666666666,
0.26666666666666666,
0.2857142857142857,
0.42857142857142855,
0.2857142857142857,
0.5833333333333334,
0.16666666666666666,
0.25,
0.5384615384615384,
0.3076923076923077,
0.15384615384615385,
0.4,
0.0,
0.6,
0.5333333333333333,
0.3333333333333333,
0.13333333333333333,
0.4117647058823529,
0.11764705882352941,
0.47058823529411764,
0.35714285714285715,
0.35714285714285715,
0.2857142857142857,
0.43478260869565216,
0.17391304347826086,
0.391304347826087,
0.3684210526315789,
0.42105263157894735,
0.21052631578947367,
0.375,
0.375,
0.25,
0.26666666666666666,
0.4,
0.3333333333333333,
0.3125,
0.25,
0.4375,
0.4,
0.2,
0.4,
0.19047619047619047,
0.42857142857142855,
0.38095238095238093,
0.15789473684210525,
0.3684210526315789,
0.47368421052631576,
0.3076923076923077,
0.38461538461538464,
0.3076923076923077,
0.3888888888888889,
0.3333333333333333,
0.2777777777777778,
0.25,
0.375,
0.375,
0.2,
0.25,
0.55,
0.3333333333333333,
0.16666666666666666,
0.5,
0.14285714285714285,
0.42857142857142855,
0.42857142857142855,
0.32,
0.44,
0.24,
0.2857142857142857,
0.42857142857142855,
0.2857142857142857,
0.5,
0.25,
0.25,
0.47619047619047616,
0.3333333333333333,
0.19047619047619047,
0.3333333333333333,
0.3333333333333333,
0.3333333333333333,
0.3076923076923077,
0.07692307692307693,
0.6153846153846154,
0.3125,
0.375,
0.3125,
0.5625,
0.1875,
0.25,
0.3333333333333333,
0.2857142857142857,
0.38095238095238093,
0.2222222222222222,
0.3888888888888889,
0.3888888888888889,
0.23529411764705882,
0.4117647058823529,
0.35294117647058826,
0.3333333333333333,
0.3333333333333333,
0.3333333333333333,
0.47058823529411764,
0.23529411764705882,
0.29411764705882354,
0.3684210526315789,
0.3157894736842105,
0.3157894736842105,
0.46153846153846156,
0.19230769230769232,
0.34615384615384615,
0.2631578947368421,
0.3157894736842105,
0.42105263157894735,
0.4117647058823529,
0.23529411764705882,
0.35294117647058826,
0.3157894736842105,
0.3684210526315789,
0.3157894736842105,
0.30434782608695654,
0.2608695652173913,
0.43478260869565216,
0.45454545454545453,
0.45454545454545453,
0.09090909090909091,
0.3,
0.35,
0.35,
0.45454545454545453,
0.18181818181818182,
0.36363636363636365,
0.21428571428571427,
0.42857142857142855,
0.35714285714285715,
0.2857142857142857,
0.35714285714285715,
0.35714285714285715,
0.23529411764705882,
0.4117647058823529,
0.35294117647058826,
0.3157894736842105,
0.3157894736842105,
0.3684210526315789,
0.36363636363636365,
0.22727272727272727,
0.4090909090909091,
0.2222222222222222,
0.3333333333333333,
0.4444444444444444,
0.2727272727272727,
0.18181818181818182,
0.5454545454545454,
0.19047619047619047,
0.5238095238095238,
0.2857142857142857,
0.391304347826087,
0.2608695652173913,
0.34782608695652173,
0.42857142857142855,
0.21428571428571427,
0.35714285714285715,
0.3333333333333333,
0.16666666666666666,
0.5,
0.23529411764705882,
0.35294117647058826,
0.4117647058823529,
0.16666666666666666,
0.5,
0.3333333333333333,
0.3,
0.2,
0.5,
0.2631578947368421,
0.3684210526315789,
0.3684210526315789,
0.4,
0.3333333333333333,
0.26666666666666666]
For completeness, let’s compute the probabilities for the parents.
[80]:
def get_prob(n, d, df):
N = df.shape[0]
p = {v: df.query(f'{n}=="{v}"').shape[0] / N for v in d}
return list(p.values())
cpt_x1 = get_prob('x1', domains['x1'], df)
cpt_x2 = get_prob('x2', domains['x2'], df)
cpt_x3 = get_prob('x3', domains['x3'], df)
cpt_x4 = get_prob('x4', domains['x4'], df)
[81]:
cpt_x1
[81]:
[0.489, 0.511]
[82]:
cpt_x2
[82]:
[0.34, 0.341, 0.319]
[83]:
cpt_x3
[83]:
[0.477, 0.523]
[84]:
cpt_x4
[84]:
[0.209, 0.197, 0.206, 0.191, 0.197]
Let’s build the BBN and join tree.
[93]:
x1 = BbnNode(Variable(0, 'x1', domains['x1']), cpt_x1)
x2 = BbnNode(Variable(1, 'x2', domains['x2']), cpt_x2)
x3 = BbnNode(Variable(2, 'x3', domains['x3']), cpt_x3)
x4 = BbnNode(Variable(3, 'x4', domains['x4']), cpt_x4)
x5 = BbnNode(Variable(4, 'x5', domains['x5']), cpt_x5)
bbn = Bbn() \
.add_node(x1) \
.add_node(x2) \
.add_node(x3) \
.add_node(x4) \
.add_node(x5) \
.add_edge(Edge(x1, x5, EdgeType.DIRECTED)) \
.add_edge(Edge(x2, x5, EdgeType.DIRECTED)) \
.add_edge(Edge(x3, x5, EdgeType.DIRECTED)) \
.add_edge(Edge(x4, x5, EdgeType.DIRECTED))
join_tree = InferenceController.apply(bbn)
Here are the posteriors.
[94]:
for node in join_tree.get_bbn_nodes():
potential = join_tree.get_bbn_potential(node)
print(node)
print(potential)
print('-' * 15)
1|x2|v0,v1,v2
1=v0|0.34000
1=v1|0.34100
1=v2|0.31900
---------------
2|x3|v0,v1
2=v0|0.47700
2=v1|0.52300
---------------
3|x4|v0,v1,v2,v3,v4
3=v0|0.20900
3=v1|0.19700
3=v2|0.20600
3=v3|0.19100
3=v4|0.19700
---------------
4|x5|v0,v1,v2
4=v0|0.33850
4=v1|0.30794
4=v2|0.35356
---------------
0|x1|v0,v1
0=v0|0.48900
0=v1|0.51100
---------------
Let’s assert evidence and observe the posteriors.
[98]:
from pybbn.graph.jointree import EvidenceBuilder
ev1 = EvidenceBuilder() \
.with_node(join_tree.get_bbn_node_by_name('x1')) \
.with_evidence('v0', 1.0) \
.build()
join_tree.unobserve_all()
join_tree.update_evidences([ev1])
for node in join_tree.get_bbn_nodes():
potential = join_tree.get_bbn_potential(node)
print(node)
print(potential)
print('-' * 15)
1|x2|v0,v1,v2
1=v0|0.34000
1=v1|0.34100
1=v2|0.31900
---------------
2|x3|v0,v1
2=v0|0.47700
2=v1|0.52300
---------------
3|x4|v0,v1,v2,v3,v4
3=v0|0.20900
3=v1|0.19700
3=v2|0.20600
3=v3|0.19100
3=v4|0.19700
---------------
4|x5|v0,v1,v2
4=v0|0.36050
4=v1|0.29824
4=v2|0.34126
---------------
0|x1|v0,v1
0=v0|1.00000
0=v1|0.00000
---------------
Let’s assert multiple evidences. The posterior for \(X_5\) should be the same as the CPT when all parents are set to v0
.
[99]:
ev1 = EvidenceBuilder() \
.with_node(join_tree.get_bbn_node_by_name('x1')) \
.with_evidence('v0', 1.0) \
.build()
ev2 = EvidenceBuilder() \
.with_node(join_tree.get_bbn_node_by_name('x2')) \
.with_evidence('v0', 1.0) \
.build()
ev3 = EvidenceBuilder() \
.with_node(join_tree.get_bbn_node_by_name('x3')) \
.with_evidence('v0', 1.0) \
.build()
ev4 = EvidenceBuilder() \
.with_node(join_tree.get_bbn_node_by_name('x4')) \
.with_evidence('v0', 1.0) \
.build()
join_tree.unobserve_all()
join_tree.update_evidences([ev1, ev2, ev3, ev4])
for node in join_tree.get_bbn_nodes():
potential = join_tree.get_bbn_potential(node)
print(node)
print(potential)
print('-' * 15)
1|x2|v0,v1,v2
1=v0|1.00000
1=v1|0.00000
1=v2|0.00000
---------------
2|x3|v0,v1
2=v0|1.00000
2=v1|0.00000
---------------
3|x4|v0,v1,v2,v3,v4
3=v0|1.00000
3=v1|0.00000
3=v2|0.00000
3=v3|0.00000
3=v4|0.00000
---------------
4|x5|v0,v1,v2
4=v0|0.46667
4=v1|0.26667
4=v2|0.26667
---------------
0|x1|v0,v1
0=v0|1.00000
0=v1|0.00000
---------------
[ ]: