Reading a fitted TRAM-DAG¶
A TRAM-DAG is a normalizing flow, but its design keeps parts of it readable. This page collects the read-outs and says what each number means. For the model itself see model.md, and for the symbols see notation.md.
Code that runs checks the claims on this page. The links say where.
Which term owns which edge¶
flow.to_matrix() gives the labeled adjacency matrix of the model. Rows are
parents and columns are children. A cell holds the term tag: LS, CS, CI,
VC for a treatment edge, or VCm for a varying-coefficient modifier. An
empty cell means no edge. A multi-parent term carries its parent group as a
suffix, and several terms in one cell join with +.
This is the meta-adjacency view of the paper. It is also the fastest way to
confirm that the model you wrote is the model you meant.
plot_dag(spec) draws the same thing, with one edge style per term.
Exercised in the varying-coefficients notebook.
A linear shift is a log-odds ratio¶
flow.ls_coefficients() gives {node: {parent: weights}} for the LS terms
only. A CS or VC shift is a network and has no single weight, so
flow.ls_coefficients() skips them.
Two things decide how to read a weight.
The sign follows the latent-scale convention of notation.md. A continuous node adds its shift, so a parent that raises the child gets a negative weight. An ordinal node subtracts its shift, so the sign is the one you expect there.
The scale is the latent scale, not the data scale. For a continuous node the weights carry the transform's scaling. A single weight is therefore not comparable to a regression coefficient. Their ratio is comparable, because the scaling cancels. An ordinal node has no such scaling, so \(\exp(\beta)\) is an odds ratio directly.
An ordinal parent gives one weight per level, from its one-hot encoding.
The data fix those weights only up to a common constant. The one-hot columns
sum to one in every row. Adding a constant to all of them, and subtracting it
from the intercept, leaves the likelihood unchanged. Read them as
differences.
w[k] - w[0] is the level-k-against-level-0 log-odds ratio. It matches the
coefficient of column {parent}[k] in
flow.design_matrix(df, node, drop_first=True), which drops the level-0
column.
Checked in notebooks/classical_fit_tram_dag.py,
which compares the weights against statsmodels and R, and pinned in
continuous integration by experiments/triangle_mixed.py, whose
odds-ratio check is in paper-replication.md.
Ordinal nodes: cutpoints and the interventional PMF¶
An ordinal node's intercept is its cutpoint vector, not a transform
(model.md). tramdag.transforms.ordinal_cutpoints
turns the unconstrained parameters into the increasing cutpoints, which is how
you read a fitted baseline.
flow.pmf(df, node, do=...) gives the class probabilities per row, in closed
form. It needs no sampling, and do= makes it interventional by overriding
columns before the parents are read. With that combination, you can answer a
treatment-effect question exactly, not by Monte Carlo.
Exercised in the classical-fitting notebook,
and under do= in experiments/triangle_mixed.py. Every run checks that script
against committed ground truth.
Continuous nodes: density and one fitted shift¶
flow.density(df, node, grid, do=...) is the continuous counterpart of
pmf. For every row it evaluates \(p(\text{node} = g \mid \mathrm{pa})\) at
each grid value, in closed form from the transform. Use it to look at a fitted
conditional without sampling it.
flow.shift_curve(node, parent, grid) evaluates one fitted shift term along a
grid of parent values. Use it to plot a CS term against the function that it
has to learn. It goes through the term's own evaluation, so a custom term
works too.
A caution on both: the data fix a CS term only up to a constant, because
that constant can move into the intercept. Compare a fitted curve to the truth
after mean-centering both, not point by point.
density runs in the
classical-fitting notebook, section
1b. experiments/triangle.py compares the shift_curve of each CS variant
with the true function against committed ground truth.
The price of a complex intercept¶
I(...) with parents is the flexible end of the family. The parents control
the transform parameters, so the node can bend in ways no additive shift can
express. The cost is that there is no single coefficient to read.
paper-replication.md measures that trade-off on the
paper's triangle process, fitted once with an LS edge and once with a CS
edge.
A middle option exists. I("a", "b", allow_interaction=False) builds one
network per parent and sums their parameter vectors, so the parents act
additively on the transform. flow.intercept_contributions(df, node) then
decomposes that sum into mean-centered per-term parts, using the usual additive
model convention, so the parts become comparable. This is a post-hoc read-out
and changes nothing about the fitted model.
Worked through in the intercept notebook.
An effect that varies with a covariate¶
VC(*modifiers, t=...) fits an effect that varies with the modifiers.
varying-coefficients.md is the guide. Before you
declare a VC term, flow.effect_modifier_scan shortlists the modifiers on a
cheap all-LS fit (scores.md).