SpendProbe.mixes_channels#

SpendProbe.mixes_channels(*, non_date_dims)[source]#

Measure whether one channel’s spend moves another channel’s column.

The separable readout takes column m of a single all-channels counterfactual to be channel m’s unilateral counterfactual. That holds only while \(v_{t,c}\) is a function of channel c’s spend alone, and nothing in the MMM enforces it: forward_pass hands the saturation the whole (date, channel) tensor, so a transform with a shared denominator – \(x_c / (1 + \sum_{c'} x_{c'})\), channels competing for one pool of attention – is expressible and makes that readout wrong for every channel at once.

Neither of the other guards can see it. measure() collapses every non-date dimension before comparing, so a move that stays within a date is invisible to it, and assert_increment_is_complete() sums over channels, which is exactly the operation the cross-column movement is conserved under. So it is measured here, and separately: one probe per spending channel, perturbing that channel alone at a date it spends, checking that no other channel’s column moves. Per channel rather than once, because mixing need not be symmetric – a single probe on the channel nothing leaks into would come back clean.

The cost is one call to the already-compiled evaluator per spending channel, so callers should ask only when a per-channel column is going to be read.

Parameters:
non_date_dimsmapping

Per node, the dimensions its evaluation carries after sample and date. Only channel_contribution’s entry is read, to find which axis the channels lie along: it is not always the last one, and an assumption there would silently compare the wrong slices.

Returns:
bool

Whether the channels have to be perturbed one at a time. True also when some spending channel could not be probed: correctness beats speed, and per-channel scenarios are right either way while assuming separability is silently wrong.