Algebra
Named-variable distributions multiply and divide according to the chain rule. The free/given metadata on each operand determines the result.
Throughout, start from named xor:
In [1]: from dit import Distribution
In [2]: d = Distribution(['000', '011', '101', '110'], [1/4]*4)
In [3]: d.set_rv_names('XYZ')
Marginal and condition
marginal() keeps the listed free variables;
marginalize() drops them. Both accept positional names
or a single list:
In [4]: d.marginal('X', 'Y')
Out[4]: <Distribution p(X,Y)>
In [5]: d.marginalize('X', 'Y')
Out[5]: <Distribution p(Z)>
Native condition_on() takes positional variable names and
returns a single conditional distribution:
In [6]: p_z_xy = d.condition_on('X', 'Y')
In [7]: p_z_xy
Out[7]: <Distribution p(Z|X,Y)>
In [8]: p_z_xy.is_conditional()
Out[8]: True
Multiplication (chain rule)
Multiplying a marginal by a compatible conditional reconstructs the joint:
In [9]: p_xy = d.marginal('X', 'Y')
In [10]: rebuilt = p_xy * p_z_xy
In [11]: rebuilt
Out[11]: <Distribution p(X,Y,Z)>
In [12]: rebuilt.is_approx_equal(d)
Out[12]: True
Partial application leaves unmatched given variables given:
In [13]: d.marginal('X') * p_z_xy
Out[13]: <Distribution p(X,Z|Y)>
The free variables of the two operands must be disjoint.
Distribution.from_factors() is the named constructor for the same
product.
Division (conditioning)
Dividing a joint by a marginal conditions on the marginal’s free variables:
In [14]: p_xy / d.marginal('X')
Out[14]: <Distribution p(Y|X)>
Independent product (@)
@ treats its operands as independent and takes their Cartesian product,
carrying the variable names over to the result. A name that occurs in both
operands becomes a single paired variable whose symbols join the two
contributions, so p(X,Y) @ p(X,Z) is a distribution over X, Y, Z
rather than one with X repeated:
In [15]: a = Distribution(['00', '11'], [1/2]*2, rv_names=['X', 'Y'])
In [16]: b = Distribution(['00', '01', '10', '11'], [1/4]*4, rv_names=['X', 'Z'])
In [17]: print(a @ b)
Class: Distribution
Alphabet: (('00', '01', '10', '11'), ('0', '1'), ('0', '1'))
Base: linear
x p(X,Y,Z)
('00', '0', '0') 0.125
('00', '0', '1') 0.125
('01', '0', '0') 0.125
('01', '0', '1') 0.125
('10', '1', '0') 0.125
('10', '1', '1') 0.125
('11', '1', '0') 0.125
('11', '1', '1') 0.125
Here X ranges over the four pairs (x_left, x_right); string symbols
are concatenated, anything else pairs to a tuple. Distributions whose names
were never set explicitly (the auto-generated X0, X1, …) have no
names to match on, so their outcomes are simply concatenated as before.
Numeric-outcome * is different
On unnamed one-dimensional numeric distributions, +, *, %, and
friends transform outcomes (two dice, scaling a die, …). See
Numeric Outcomes. Named-variable * is always the chain rule.
Compatibility condition_on
Passing a list, or the crvs / rvs keywords, uses the older
dit-compatible return format (marginal, list_of_slices) — one
distribution per outcome of the conditioning variables. Prefer the native
form above; the tuple form is still used by some algorithms.
In [18]: marg, cdists = d.condition_on(['X', 'Y'], rvs=['Z'])
In [19]: marg
Out[19]: <Distribution p(X,Y)>
In [20]: len(cdists)
Out[20]: 4
API
- Distribution.marginal(*args)[source]
Marginalise to keep only the specified free variables.
Given (conditioned) variables are always kept.
Supports two call signatures:
marginal('X', 'Y')– positional variable namesmarginal(['X', 'Y'])– list of names (or integer indices)
- Parameters:
*args (str, or a single list/tuple) – The free variable names to keep. Integer indices are auto-resolved to dimension names.
- Returns:
result
- Return type:
Distribution
- Distribution.marginalize(*args)[source]
Marginalise out (remove) the specified free variables.
Supports two call signatures:
marginalize('X')– positional variable namesmarginalize(['X'])– list of names (or integer indices)
- Parameters:
*args (str, or a single list/tuple) – The free variable names to remove.
- Returns:
result
- Return type:
Distribution
- Distribution.condition_on(*cond_vars, rvs=None, crvs=None)[source]
Condition on the specified free variables.
Supports two call signatures:
Native:
condition_on('X', 'Y')– positional var names. Returns a single conditionalDistribution.dit-compat: any of these forms triggers the dit-compatible return format
(marginal, list_of_conditionals):condition_on(crvs=['X'], rvs=['Y'])condition_on(['X'], rvs=['Y'])(positional crvs)condition_on(crvs=['X'])
The returned list contains one
Distributionper outcome of the conditioning variable.
- Parameters:
*cond_vars (str, or a single list/tuple) – Variable names to condition on. If a single list/tuple is passed and
rvsis provided, it is interpreted ascrvs(dit-compat positional form).rvs (list, optional) – Variables to keep in the conditional (dit-compat API).
crvs (list, optional) – Variables to condition on (dit-compat API).
- Returns:
result – A single conditional distribution (native), or a
(marginal, list_of_Distributions)tuple (dit-compat).- Return type:
Distribution or tuple
Examples
>>> p_xyz.condition_on('Z') # native: returns p(X,Y|Z) >>> p_xyz.condition_on('X', 'Y') # native: returns p(Z|X,Y) >>> marg, cdists = p_xyz.condition_on(crvs=['Z']) # dit-compat >>> marg, cdists = p_xyz.condition_on(['Z'], rvs=['X']) # dit-compat
- Distribution.coalesce(rvs, extract=False)[source]
Return a new distribution after coalescing random variables.
Each inner sequence in rvs defines one new random variable as a combination of original variables. The result is a joint
Distributionoverlen(rvs)new random variables whose outcomes are tuples (or the inner values whenextract=Truewith a single group).- Parameters:
rvs (sequence of sequences) – Each inner sequence contains variable names (or integer indices).
extract (bool) – If
Trueandlen(rvs) == 1, the single group’s values are used directly as outcomes instead of being wrapped in 1-tuples.
- Returns:
d
- Return type:
Distribution