ump_spec.rules.estimands
module ump-spec
Estimand rules (spec §5): canonical form, compare, align ladder (G-EST-02), fill undeclared with the rule 4 envelope (G-EST-01), plain words.
Classes
Section titled “Classes”CanonicalEstimand
Section titled “CanonicalEstimand”class
class CanonicalEstimandSpec §5 canonical form. fields maps each of the seven fields to its canonical value, or None
when undeclared (rule (c): None never equals anything). sign is -1 when the source was in the
reverse orientation, so its quantity must be negated. steps names the rules applied.
Fields
| Name | Type | Default | Description |
|---|---|---|---|
fields | dict[str, Any] | ||
scope | dict[str, Any] | ||
sign | int | 1 | |
steps | tuple[str, ...] | () |
Methods and properties (1)
init constructor
CanonicalEstimand( self, fields: dict[str, Any], scope: dict[str, Any], sign: int = 1, steps: tuple[str, ...] = (),) -> NoneSource: packages/ump-spec/src/ump_spec/rules/estimands.py, line 51
EstimandComparison
Section titled “EstimandComparison”class
class EstimandComparisoncombinable only when every canonical field is declared and equal, the scopes are equal and
both sides share one orientation (rule 1). scope is contained when one geo set strictly
contains the other (aligned, not equal); sign_flip when one side is in the reverse orientation.
Fields
| Name | Type | Default | Description |
|---|---|---|---|
combinable | bool | ||
mismatched | list[str] | ||
undeclared | list[str] | field(default_factory=list) | |
details | dict[str, tuple[Any, Any]] | field(default_factory=dict) | |
scope | ScopeRelation | 'equal' | |
sign_flip | bool | False | |
steps | list[str] | field(default_factory=list) |
Methods and properties (1)
init constructor
EstimandComparison( self, combinable: bool, mismatched: list[str], undeclared: list[str] = list(), details: dict[str, tuple[Any, Any]] = dict(), scope: ScopeRelation = 'equal', sign_flip: bool = False, steps: list[str] = list(),) -> NoneSource: packages/ump-spec/src/ump_spec/rules/estimands.py, line 150
Converter
Section titled “Converter”class
class ConverterFields
| Name | Type | Default | Description |
|---|---|---|---|
name | str | ||
field | str | ||
description | str | ||
needs | tuple[str, ...] | ||
per_draw | bool | False |
Methods and properties (2)
applies method
def applies(self, ea: Estimand, eb: Estimand) -> boolinit constructor
Converter( self, name: str, field: str, description: str, needs: tuple[str, ...], per_draw: bool = False,) -> NoneSource: packages/ump-spec/src/ump_spec/rules/estimands.py, line 195
AlignmentResult
Section titled “AlignmentResult”class
class AlignmentResultrule is the spec §5 combination rule used (1, 2.1 to 2.4); resummary marks
the rule 2.2 re-summary of draws; scope is the comparison’s scope relation.
Fields
| Name | Type | Default | Description |
|---|---|---|---|
step | AlignStep | ||
combinable | bool | ||
mismatched | list[str] | ||
converters | list[str] | field(default_factory=list) | |
fallback | FallbackEntry | None | None | |
notes | list[Note] | field(default_factory=list) | |
interval | Interval | None | None | |
reason | str | '' | |
rule | str | '' | |
resummary | bool | False | |
scope | ScopeRelation | 'equal' |
Methods and properties (1)
init constructor
AlignmentResult( self, step: AlignStep, combinable: bool, mismatched: list[str], converters: list[str] = list(), fallback: FallbackEntry | None = None, notes: list[Note] = list(), interval: Interval | None = None, reason: str = '', rule: str = '', resummary: bool = False, scope: ScopeRelation = 'equal',) -> NoneSource: packages/ump-spec/src/ump_spec/rules/estimands.py, line 240
Functions
Section titled “Functions”canonical_estimand
Section titled “canonical_estimand”function
def canonical_estimand(estimand: Estimand | dict[str, Any]) -> CanonicalEstimandReduce an estimand to the spec §5 canonical form before G-EST-02 compares it:
- (a)
scalewith relative size -1.0 againstcurrent_planbecomesremove_allagainstcurrent_plan; the reverse orientation (remove_allor factor 0 againstzero_spend, the ROI-versus-zero-spend form adapters write) takes the same form withsign-1; - (b)
totaland atotal_to_decaywith no threshold takeTOTAL_TO_DECAY_DEFAULT_THRESHOLD; - (d) interval levels are rounded to three decimals;
- (e) a
schema_versionthat is not a registered estimand registry version is undeclared; - (f) a bare
point_rulename becomes{rule, scale: identity}.
The scope keeps every applies_to member, with media_ids and geo_ids as sorted sets.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 77
compare_estimands
Section titled “compare_estimands”function
def compare_estimands( a: Estimand | dict[str, Any], b: Estimand | dict[str, Any],) -> EstimandComparisonCompare two estimands in canonical form (spec §5). Undeclared fields count as mismatched and are
also listed in undeclared. A strict geo containment is reported in scope, not mismatched.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 165
function
def align( a: Estimand | dict[str, Any], b: Estimand | dict[str, Any], *, a_is_model: bool = False, b_is_model: bool = False, available_inputs: set[str] | None = None, can_reask: bool = True, candidate_intervals: list[Interval] | None = None, a_has_draws: bool = False, b_has_draws: bool = False, mode: Literal['fallback', 'strict'] = 'fallback',) -> AlignmentResultG-EST-02 ladder on the canonical forms: converter -> re-ask the model (or, when both sides carry
draws and only point_rule or interval differ, a re-summary of the draws: rule 2.2, no
widening) -> assumed alignment with envelope widening -> side by side. Records the step used.
A reverse orientation is the orientation_sign_flip converter step. A strict geo containment is
aligned, not equal: ESTIMAND_ALIGNED and the representativeness check. candidate_intervals
(one per plausible alignment) gives the envelope for an assumed alignment. A remove_all vs
marginal intervention mismatch (ROI vs mROI) is never assumed away: it goes side by side.
Strict mode raises ESTIMAND_ALIGNED (409) for any rung after the converter.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 276
max_alternatives
Section titled “max_alternatives”function
def max_alternatives() -> intThe cap in force: MAX_ALTERNATIVES (tests/test_estimands.py pins it to the registry).
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 421
widen_factor
Section titled “widen_factor”function
def widen_factor(source_type: str | None = None) -> floatProposed widening for an assumed field with no registered alternatives: the source type’s own
widen_factor, else the registry’s envelope.widen_factor, else DEFAULT_UNDECLARED_WIDEN.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 426
plausible_alternatives
Section titled “plausible_alternatives”function
def plausible_alternatives( estimand: Estimand | dict[str, Any], source_type: str | None = None,) -> list[Estimand] | NoneThe rule 4 plausible set: one filled estimand per element of the cross product of the registered
alternatives of every assumed field, the registered default first. None when the product
exceeds the registered max_alternatives (the step then goes side by side). An estimand with
nothing assumed gives a list of itself.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 539
fill_undeclared
Section titled “fill_undeclared”function
def fill_undeclared( estimand: Estimand | dict[str, Any], source_type: str | None = None, *, interval: Interval | None = None, alternative_intervals: list[Interval] | None = None, score: Callable[[Estimand], Interval | None] | None = None, mode: Literal['fallback', 'strict'] = 'fallback',) -> tuple[Estimand, list[FallbackEntry], Interval | None]G-EST-01 under spec §5 rule 4. Each undeclared field takes the registered default for the source
type and is marked assumed; a field with no registered default stays undeclared and goes side
by side (there are no global defaults). The interval:
- the envelope (method
envelope, the declared level kept) overscore(alternative)for each element of the registered alternatives’ cross product (plausible_alternatives), or overintervalandalternative_intervalswhen the caller scored them itself; - beyond
max_alternativesthe step goes side by side: nothing is assumed, the interval is kept; - with no registered alternatives (or none scored),
intervalwidened bywiden_factoraround its midpoint, methodimputed.
The point stays the caller’s value under the registered default, labelled assumed. Strict mode
raises ESTIMAND_UNDECLARED (422).
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 551
reallocation_intervention
Section titled “reallocation_intervention”function
def reallocation_intervention( source: str, destination: str, *, amount: float | None = None, share: float | None = None, population: str | None = None,) -> InterventionHow to express a budget shift between channels without a fifth intervention type.
Spec §5 fixes four intervention types (remove_all, scale, add, marginal), so a reallocation is written as a two-arm intervention against the current plan:
- a known amount moved:
{type: "add", size: amount, size_kind: "absolute", arms: ["from:<src>", "to:<dst>"]}; - a share of the source’s spend moved:
{type: "scale", size: -share, size_kind: "relative", arms: [...]}.
Pair it with baseline="current_plan" and spend_unit(currency) (per unit of currency moved). Total spend
is unchanged. compare_estimands treats two reallocations as matching only when type, size and both
arms match. plain_words renders it as “moving 10% of search spend from search to social”.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 651
reallocation_arms
Section titled “reallocation_arms”function
def reallocation_arms( iv: Intervention | dict[str, Any] | None,) -> tuple[str, str] | None(sources, destinations) when the intervention uses the from:/to: arm convention.
Every arm is kept: with several from: or to: arms the names are joined with ”, ” in arm order
(["from:tv", "from:social", "to:search"] gives ("tv, social", "search")).
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 674
plain_words
Section titled “plain_words”function
def plain_words(estimand: Estimand | dict[str, Any]) -> strRender the estimand in plain words, naming every assumed or undeclared field (§5 rule 5). Example: “revenue per dollar from removing all spend versus zero spend (assumed; the source did not say), total including carryover, posterior median, 90% eti”.
Source: packages/ump-spec/src/ump_spec/rules/estimands.py, line 687
Constants
Section titled “Constants”| Name | Type | Value | Description |
|---|---|---|---|
AlignStep | Literal['match', 'converter', 're_ask', 'assumed_alignment', 'side_by_side'] | ||
ALIGN_LADDER | tuple[str, ...] | ('converter', 're_ask', 'assumed_alignment', 'side_by_side') | |
SCOPE_FIELD | 'applies_to' | ||
COMPARED_FIELDS | tuple[str, ...] | (*ESTIMAND_FIELDS, SCOPE_FIELD) | |
TOTAL_TO_DECAY_DEFAULT_THRESHOLD | 0.05 | Proposed registered default decay threshold (0.05 of the peak adstock weight, spec §5 rule (b)): total and a total_to_decay with no threshold take it. | |
ORIENTATION_STEP | 'orientation_sign_flip' | Converter step recorded when one side is in the reverse orientation (spec §5 rule (a)). | |
ScopeRelation | Literal['equal', 'contained', 'differ'] | ||
CONVERTERS | tuple[Converter, ...] | (Converter('immediate_to_total_adstock', 'horizon', 'immediate to total via t... | |
ASSUMABLE_FIELDS | frozenset[str] | frozenset({'baseline', 'horizon', 'point_rule', 'interval', 'schema_version',... | Fields where an assumed alignment with envelope widening is defensible. A different KPI, unit denominator or scope is never assumed away: those go side by side. |
RESUMMARY_FIELDS | frozenset[str] | frozenset({'point_rule', 'interval'}) | Rule 2.2: when both sides carry draws and only these differ, the draws are re-summarised. |
NON_ASSUMABLE_INTERVENTION_PAIRS | frozenset[frozenset[str]] | frozenset({frozenset({'remove_all', 'marginal'})}) | Intervention-type pairs that an assumed alignment never bridges (MOD-EST-03): a remove-all ROI against zero spend and a marginal ROI at the current plan are different numbers, so they go side by side unless a registered converter or a re-ask answers the other question. |
MAX_ALTERNATIVES | 8 | Proposed cap on the cross product of assumed-field alternatives (spec §5 rule 4); mirrors envelope.max_alternatives in registries/source_types.yaml. Beyond it the step goes side by side. | |
REALLOCATION_PREFIXES | ('from:', 'to:') |