cleverly.learners.ConditionalDensity#

class cleverly.learners.ConditionalDensity(bin_probabilities: ndarray[tuple[Any, ...], dtype[float64]], edges: ndarray[tuple[Any, ...], dtype[float64]])[source]#

An evaluated conditional density: one row of bin probabilities per observation.

Like RegimeSet, this holds evaluated arrays and no callables. The density is estimated once, out of fold, and stored as the (n, B) matrix \(P(b(A) = b \mid W_i)\); every later evaluation – at the observed treatment, at a shifted one, at a grid for a plot – is a lookup into row i, not a model call.

That is not only an efficiency: it is what makes “g(A | W) and g(A - delta | W) come from the same out-of-fold model” a structural fact rather than an invariant somebody has to maintain. It is also what lets a fit survive load(), a bootstrap resample and every retarget() without the learner being refit.

Parameters:
bin_probabilitiesndarray

(n, B), rows summing to one. Out-of-fold predictions.

edgesndarray

(B + 1,) bin boundaries, strictly increasing.

Methods#

ConditionalDensity.bin_of(values: ndarray[tuple[Any, ...], dtype[float64]]) → ndarray[tuple[Any, ...], dtype[int64]][source]#

Which bin each value falls in; -1 outside the support.

-1 rather than a clamp, so a caller that evaluates outside the observed range gets a zero density and can see that it did, instead of silently reading the nearest edge bin.

Parameters:
valuesndarray

(n,) treatment values, one per row.

Returns:
ndarray

(n,) bin index per row, and -1 where the value is outside the support.

ConditionalDensity.crossing_fraction(shifted: ndarray[tuple[Any, ...], dtype[float64]], observed: ndarray[tuple[Any, ...], dtype[float64]]) → float[source]#

Share of rows a shift actually moves into a different bin.

The resolution guard described in the module docstring: a shift that moves nobody across an edge has a clever covariate of exactly one everywhere and estimates nothing, however clean the fit looks.

Parameters:
shiftedndarray

(n,) dose the policy assigns.

observedndarray

(n,) dose actually received.

Returns:
float

Share of rows whose shifted dose falls in a different bin. A shift that moves nobody across a bin boundary is invisible to a binned density.

ConditionalDensity.density_at(values: ndarray[tuple[Any, ...], dtype[float64]]) → ndarray[tuple[Any, ...], dtype[float64]][source]#

\(\hat g(a_i \mid W_i)\) for one value per row; zero outside the support.

A pure lookup into bin_probabilities, so this can be called as often as an estimand needs without refitting anything.

Parameters:
valuesndarray

(n,) treatment values, one per row.

Returns:
ndarray

(n,) density at those values, and zero outside the support.

ConditionalDensity.integrated() → ndarray[tuple[Any, ...], dtype[float64]][source]#

(n,) values of \(\int \hat g(a \mid W_i)\, da\), which must all be one.

The hazard product is constructed to sum to one exactly, so this is a check on the arithmetic rather than on the model; it is reported because a silent failure here would rescale every clever covariate by an amount no other diagnostic would see.

Returns:
ndarray

(n,) integral of the fitted density over the support, per row.

ConditionalDensity.subset(index: Any) → ConditionalDensity[source]#

The same density on a row subset – a bootstrap resample, a validation fold.

The rows are sliced and the edges kept, exactly as subset() slices its densities: re-deriving the edges from the resample would give the subset a different discretisation from the fit it is meant to be a replicate of.

Parameters:
indexarray_like

Row positions or a boolean mask.

Returns:
ConditionalDensity

The same density over the selected rows.

Attributes#

ConditionalDensity.n#

Return the number of observations.

ConditionalDensity.n_bins#

Return the number of bins the treatment support is divided into.

Returns:
int

Bin count, which is the second axis of bin_probabilities.

ConditionalDensity.support#

(lower, upper) – the range the density is nonzero on.

ConditionalDensity.widths#

(B,) bin widths, the Jacobian turning a probability into a density.

ConditionalDensity.bin_probabilities: ndarray[tuple[Any, ...], dtype[float64]]#
ConditionalDensity.edges: ndarray[tuple[Any, ...], dtype[float64]]#