Conditioning and inference¶
Conditionalization¶
conditionalization() turns a joint state into
a kernel. concat_start_index is the 1-based index of the first codomain
factor that becomes the new codomain; the preceding factors become the
domain. A slice whose codomain sum is zero stays zero, and
is_markov() treats that row as allowed. The
receiver must be a state.
import numpy as np
from fxtensor_salmon import FXTensor
location_labels = ["Urban", "Rural"]
season_labels = ["Spring", "Summer", "Other"]
weather_labels = ["Sunny", "Rainy"]
joint = FXTensor(
[[], [location_labels, season_labels, weather_labels]],
data=np.array(
[
[[0.08, 0.04], [0.12, 0.04], [0.08, 0.04]],
[[0.06, 0.12], [0.12, 0.12], [0.06, 0.12]],
]
),
)
cond_tensor = joint.conditionalization(concat_start_index=3)
assert cond_tensor.labels == ([location_labels, season_labels], [weather_labels])
assert cond_tensor.is_markov()
assert np.allclose(
cond_tensor.data,
[
[[2 / 3, 1 / 3], [0.75, 0.25], [2 / 3, 1 / 3]],
[[1 / 3, 2 / 3], [0.50, 0.50], [1 / 3, 2 / 3]],
],
)
joint_state = FXTensor([[], [2, 2]], data=np.array([[0.1, 0.2], [0.0, 0.0]]))
kernel = joint_state.conditionalization(2)
assert kernel.profile == [[2], [2]]
assert kernel.is_markov()
assert np.allclose(kernel.data, [[1 / 3, 2 / 3], [0.0, 0.0]])
not_a_state = FXTensor([[2], [2]], data=np.eye(2))
try:
not_a_state.conditionalization(1)
except ValueError:
pass
else:
raise AssertionError("conditionalization requires a state")
Conditional¶
conditional() is the same split for a kernel
f: A → X ⊗ Y. The domain of the result is A ⊗ X. On a state it agrees
with conditionalization.
import numpy as np
from fxtensor_salmon import FXTensor
location_labels = ["Urban", "Rural"]
season_labels = ["Spring", "Summer", "Other"]
weather_labels = ["Sunny", "Rainy"]
joint = FXTensor(
[[], [location_labels, season_labels, weather_labels]],
data=np.array(
[
[[0.08, 0.04], [0.12, 0.04], [0.08, 0.04]],
[[0.06, 0.12], [0.12, 0.12], [0.06, 0.12]],
]
),
)
cond_tensor = joint.conditionalization(concat_start_index=3)
assert cond_tensor.labels == ([location_labels, season_labels], [weather_labels])
assert cond_tensor.is_markov()
assert np.allclose(
cond_tensor.data,
[
[[2 / 3, 1 / 3], [0.75, 0.25], [2 / 3, 1 / 3]],
[[1 / 3, 2 / 3], [0.50, 0.50], [1 / 3, 2 / 3]],
],
)
joint_state = FXTensor([[], [2, 2]], data=np.array([[0.1, 0.2], [0.0, 0.0]]))
kernel = joint_state.conditionalization(2)
assert kernel.profile == [[2], [2]]
assert kernel.is_markov()
assert np.allclose(kernel.data, [[1 / 3, 2 / 3], [0.0, 0.0]])
not_a_state = FXTensor([[2], [2]], data=np.eye(2))
try:
not_a_state.conditionalization(1)
except ValueError:
pass
else:
raise AssertionError("conditionalization requires a state")
Bayesian inversion¶
Given a prior state p: I → X and a kernel f: X → Y,
bayesian_inversion() returns f†: Y → X
with
f†(x|y) = p(x) f(y|x) / Σ_x' p(x') f(y|x')
Rows with zero evidence stay zero. X labels come from f when f is
labeled, otherwise from the prior. The finite equation
p ; copy ; (id ⊗ f) = p ; f ; copy ; (f† ⊗ id) holds for the tensors this
method returns.
Bayesian inversion: p ; copy ; (id ⊗ f) = p ; f ; copy ; (f† ⊗ id).¶
import numpy as np
from fxtensor_salmon import FXTensor
health = ["Sick", "Healthy"]
result = ["Positive", "Negative"]
prior = FXTensor([[], [health]], data=np.array([0.01, 0.99]))
test = FXTensor([[health], [result]], data=np.array([[0.9, 0.1], [0.05, 0.95]]))
posterior = test.bayesian_inversion(prior)
assert posterior.labels == ([result], [health])
assert np.isclose(posterior.data[0, 0], 2 / 13)
x = prior.labels[1]
y = test.labels[1]
forward = prior.composition(FXTensor.copy_tensor(x)).composition(
FXTensor.identity_tensor(x).tensor_product(test)
)
backward = prior.composition(test).composition(FXTensor.copy_tensor(y)).composition(
posterior.tensor_product(FXTensor.identity_tensor(y))
)
assert forward == backward
point = FXTensor([[], [2]], data=np.array([1.0, 0.0]))
kernel = FXTensor([[2], [2]], data=np.array([[0.0, 1.0], [0.3, 0.7]]))
inverted = kernel.bayesian_inversion(point)
assert np.allclose(inverted.data[0], [0.0, 0.0])
assert np.allclose(inverted.data[1], [1.0, 0.0])
Support and almost-sure equality¶
support() is a 0/1 tensor, same profile and
labels. Entries within FXTensor._ATOL of zero count as zero.
almost_surely_equal() asks whether two
tensors of the same profile agree wherever a prior state is positive. Labels
are not compared. The prior must be a state whose codomain matches the
domain.
is_absolutely_continuous() asks whether
self is zero everywhere other is zero. For kernels this is checked
input by input. Profiles must match; labels are not compared.
Almost-sure equality: p ; copy ; (id ⊗ f) = p ; copy ; (id ⊗ g).¶
import numpy as np
from fxtensor_salmon import FXTensor
prior = FXTensor([[], [["x0", "x1", "x2"]]], data=np.array([0.4, 0.6, 0.0]))
f = FXTensor(
[[["x0", "x1", "x2"]], [["y0", "y1"]]],
data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]),
)
g = FXTensor(
[[["x0", "x1", "x2"]], [["y0", "y1"]]],
data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]),
)
assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
assert f.almost_surely_equal(g, prior)
assert not f.almost_surely_equal(g, FXTensor([[], [3]], data=np.array([0.2, 0.2, 0.6])))
assert f.is_absolutely_continuous(g)
assert not g.is_absolutely_continuous(f)
relabeled = FXTensor([[["a", "b", "c"]], [["p", "q"]]], data=np.array(f.data))
assert f.almost_surely_equal(relabeled, prior)