Skip to content

chc.decision

Stability tier: stable.

One call from a panel of logs to a certified intervention schedule.

Every layer this needs already exists --- chc.graph for which covariates to adjust for, chc.dynamics_id for a control channel that is interventional rather than observational, chc.plan for the constrained solve and its error tube, and chc.plan.certify_safety for what the plan survives under confounding. What did not exist was a way to run them in that order without re-deriving the wiring, so the demo in every README was five imports and forty lines. prescribe is that wiring and nothing more: no new estimator, no new solver, no new guarantee.

The module is chc.decision rather than chc.prescribe for one reason: the headline export is a function named prescribe, and a module of the same name inside the same package is shadowed by it --- after import chc.prescribe, chc.prescribe is the function and chc.prescribe.Lever raises AttributeError. It was the only such collision in the library.

The causal assumption is a required argument. adjustment= takes either a CausalGraph, from which the adjustment set is derived and can come back not_identified, or an explicit sequence of column names, which asserts it. There is no default, because the default would be "adjust for nothing", which is a causal claim this library exists to stop people making by accident.

Two questions, never conflated, following chc.plan.CausalPlan. Whether the effect is identified is about the data and the graph; whether the plan is certified is about the model error and the barrier. DecisionCertificate reports both, and an unidentified effect produces no schedule at all --- Prescription.schedule raises rather than hand back actions computed from a channel nothing in the log pins down.

SCHEMA_VERSION = 2 module-attribute

to_json's schema version. Bumped when a field changes meaning, not when one is added: 2 writes a number that is not finite as null (ADR 0055).

POLICY_SCHEMA_VERSION = 1 module-attribute

PrescribedPolicy.to_json's schema version, which PrescribedPolicy.from_json reads, and no other.

IdentificationStatus = Literal['identified', 'asserted', 'not_identified'] module-attribute

How the set was arrived at. asserted means a caller named it and nothing here checked it.

TubeRate = Literal['global', 'local'] module-attribute

Where the error tube's rate bounds the field's slope in the state: at every state, the levers anywhere in their box, or at the plan's start alone.

TubeMode = Literal['exact', 'bound'] module-attribute

What each radius of the error tube is. Both add a one-standard-error scale of the fitted field's error, the channel's and the drift's, to the radius within which the plant's own noise leaves the plan at that step with probability 0.95 (ADR 0080). exact: on a field affine in the state, both carried through RK4's step matrix of the field's slope at each planned action, so the noise's radius is its exact 0.95 quantile at that step under the fitted field. bound: both carried by a bound on the norm of the slope, so the noise's radius holds at least 0.95, exactly at the first step.

TimeZero = Literal['calendar', 'unit'] module-attribute

Where Prescription.evaluate's windows start: on one calendar for every unit, or cut back from each unit's own latest period.

Estimability = Literal['estimable', 'held_to_log', 'not_estimable'] module-attribute

Whether the log determines the plan's predicted path (ADR 0054). estimable: the log moved every direction of the channel. held_to_log: it left some unmoved, and the plan keeps to what the log did along them, so every fit the log cannot tell from the fitted one predicts the same path. not_estimable: some such fit predicts another path from DecisionCertificate.first_loaded_step on, or no plan could keep to the log and none was made.

GammaStarStatus = Literal['finite', 'every_level', 'no_level'] module-attribute

What DecisionCertificate.gamma_star reads. finite: the level past which some step's barrier no admissible action keeps. every_level: inf, every step's barrier is kept however strong the confounding. no_level: nan, some step's barrier no admissible action keeps, not even at gamma = 1.

DecisionError

Bases: ValueError

A decision could not be set up: a lever, a target, a constraint or an adjustment is wrong.

Subclasses ValueError so it stays catchable the way it always was, and exists so a caller wrapping prescribe can tell its own mis-specification apart from a ValueError thrown out of jax, numpy or the solver. Every message names the offending column or bound.

Columns that are simply absent still raise KeyError, matching Panel lookup: asking for a name that is not there is a lookup failure, not a bad decision.

NotIdentifiedError

Bases: DecisionError

The effect is not identified by adjustment, so there is no schedule to hand back.

Its own type because it is the one failure this library exists to produce. A caller may want to fall back to chc.sensitivity's partial-identification path on exactly this and on nothing else; catching it by message would be catching it by accident.

LeverRelation dataclass

A combination of the levers the log kept at one level, sum_j weights[j] u_j = level, which the plan keeps at every step. weights has one entry per lever and unit length.

Lever dataclass

One actionable column and the box it may move in.

unit_cost is the quadratic price of using it, and defaults to free: with a box constraint a free lever is still bounded, so the default is safe rather than merely convenient.

cap_per_step bounds how far the lever may move between consecutive planned steps. None lets it jump anywhere in its box from one step to the next, which is a schedule a media plan, a rota or a price list often cannot execute. The move from the level already in force to the first planned step is not capped, since the planner is not told what that level is.

Target dataclass

The column to steer and where to steer it.

value is one level for the whole plan, or a schedule with one level per step: value[k] is the level for the state after k + 1 actions, so a schedule has horizon entries. A schedule is how a set point that moves inside the horizon reaches the plan --- a comfort band that tightens at eight is steered for before eight, not re-planned for after it.

Constraint dataclass

A state that must stay inside a range --- what the safety certificate is priced against.

Each finite bound is a barrier of its own, x - lo or hi - x, and a step is certified where the condition of every bound holds, the far one's as well as the near one's: the state approaches neither faster than the class-K gain times its distance to it.

state may be the target column: the steered state is then bounded as well as steered, which is how a comfort band around a set point is stated. A target value outside its own bound is allowed and means "as close as the bound permits"; holding the bound is what keeps the plan from crossing it on the way.

Driver dataclass

An exogenous column that pushes the state, and its forecast over the plan.

forecast[k] is the level at t = k * dt, so a forecast has horizon + 1 entries: one at the start of every step and one at the end of the last. Between two entries the driver moves linearly, which is how the fit reads it off the log (chc.dynamics.DrivenDynamics). Nothing the plan does moves a driver --- weather, a published demand forecast; a column the levers can move is a state, not a driver.

InterventionSchedule dataclass

What to set each lever to, step by step.

windows(*, tol=1e-06)

First and last step at which each lever is active, or None if it never is.

Derived rather than stored: a start/stop pair that can disagree with the magnitudes it summarises is a worse footgun than computing it twice.

PrescribedPolicy dataclass

The decision as a rule to run, step by step, in the state each step reaches.

A lever the plan moves takes the schedule's level at the step. A lever the log set from the state alone (DecisionCertificate.rule_levers) takes the log's rule at the state it is given, as the plan's field and its price read it (ADR 0054), so its column of schedule is nan. A lever the plan holds to the log, or one the selection left out, keeps the level the schedule holds it at. Along the path the plan predicts, the actions are Prescription.schedule's. Experimental.

horizon property

How many steps the policy runs.

ruled property

The levers set from the state on the log's rule, in the order of levers.

actions_at(step, state)

Each lever's level at step in state, in the order of levers.

state is the states' values in the order of states, or a mapping that names each of them; a name it holds besides is not read.

Raises:

Type Description
ValueError

on a step that is not an integer from 0 to horizon - 1, or a state that does not give each of states a finite value.

to_json()

The policy as plain JSON-safe values, which from_json reads back to the bit.

No number is infinite or nan (ADR 0055). A ruled lever's column of schedule is None, as its rule sets it; rule names those levers and holds the rule, an infinite bound of its as None: lo's is -inf, hi's inf. precision names the floats the rule reads the state in, None where there is no rule.

from_json(record) classmethod

The policy to_json wrote.

Raises:

Type Description
ValueError

on a record of another schema version, or one that does not run: names that are not distinct, a schedule that is not a finite number for each lever at each step, a ruled lever's None excepted, a rule that does not fit the levers and the states, or a rule read in a precision JAX does not now run in.

DecisionCertificate dataclass

What is known about the decision, on the two axes that must not be merged.

Identification is about the data and the graph; certification is about model error and the barrier. A plan can be fully certified against a channel nothing identifies --- that is a trustworthy tube around a meaningless action --- so both are reported and neither is derived from the other.

stationarity_status property

How stationarity reads against its tolerance (chc.plan.DiagnosticStatus); None where no plan was solved.

violation_status property

How constraint_violation reads against its tolerance; None where no plan was solved.

trustworthy_steps property

The prefix that survives both axes --- the number an operator can act on.

Zero whenever the effect is not identified, whatever the tube says, and never longer than the shorter of the two certified prefixes, nor past the first step a fit the log cannot tell apart predicts differently. The two None values mean different things. certified_horizon is None means the tube was not evaluated, so nothing bounds the trajectory's error and it contributes zero rather than infinity --- a barrier cleared by an unbounded trajectory proves nothing. barrier_certified_steps is None means no state was bounded, so there is no safety prefix to respect and the tube alone decides.

gamma_star_status property

What gamma_star reads (GammaStarStatus); None where no bound was audited.

SelectionStep dataclass

One step of the greedy lever selection: the lever it added, and what the plan cost with it.

regret_bound is chc.plan.plan_regret_bound on that step's plan, priced against every lever's box, the levers not yet selected included. So it bounds how far below task_cost any plan the boxes allow can go: what the levers left out at this step could still buy, plus what the solve left on the table. inf when the objective was not convex over the boxes, as in DecisionCertificate.regret_bound; regret_status says which.

LeverSelection dataclass

Which levers prescribe kept under max_levers, in the order it added them.

What a step bought is the drop in planned cost from the step before it; idle_cost is the planned cost with every lever held at zero, which is what the first step is measured against.

StartState dataclass

The state the plan starts from, or would have, and where it came from.

source is "given" for a caller's x0 and "panel" for the panel's own: the mean over its units of each unit's last logged state. A mean over units that sit far apart is a start no unit is at, so the panel's start keeps how many units it averages, the periods their last states were logged at, and each state's spread over them.

RunProvenance dataclass

What the run used, beside the panel's Provenance (ADR 0063).

The panel's record is made when the panel is built, and a run need not match it: a panel built under JAX's x64 and planned on with it off fits on float32 transitions while its record reads True. So each field here is read where the run used it: x64 as prescribe ran; the device and the fit's dtype off the transitions the fit ran on; the solve's dtype, why it stopped and after how many steps off the plan the planner returned; each setting as the fit and the planner were given it; and each version from the installed distribution's metadata.

to_json()

A plain dict, ready for json.dumps(..., allow_nan=False): the versions an object keyed by distribution, and an infinite tolerance null.

Prescription dataclass

The decision, the evidence for it, and what it took to get there.

schedule property

The actions to take.

Raises:

Type Description
ValueError

if the effect is not identified. Returning a schedule here is the exact failure this library exists to prevent: the numbers would look like every other schedule and mean nothing. Read DecisionCertificate.adjustment for why, and chc.sensitivity for what to do about it.

policy()

The decision as a rule to run step by step in the states it reaches: the schedule for the levers the plan moves, the log's rule of the state for those the log set from it.

Raises:

Type Description
NotIdentifiedError

if the effect is not identified, as schedule does.

ValueError

on a prescription prescribe did not build, which does not record its states or the rule its levers keep to.

budget_prices(tolerance=None)

What each budget is worth to the plan: one price per period it holds in the plan.

A period's price is how much the planned cost falls per unit more it may spend, in the cost's units per unit of spend; zero where the period has room. Read as chc.plan.CausalPlan.shadow_prices reads every row, whose tolerance this is.

Raises:

Type Description
NotIdentifiedError

if the effect is not identified, so no plan was made.

ValueError

if the plan was held under the constraints' barrier (hold_constraints), whose rows' prices are not built.

evaluate(panel, *, logger=None, smoothing=None, model_error=1.0, min_effective=100.0, time_zero='calendar', n_resamples=200, seed=0, max_refused=0.05)

The schedule's expected cost over its horizon, estimated from panel before it is deployed, beside the cost of the policy that logged the panel: chc.evaluation.evaluate_plan by "pdis", whose keywords the first four are, with its intervals read from a bootstrap over the units.

What it is handed: the episodes are windows of H + 1 consecutive periods of a unit, over the plan's states and the levers; the schedule is the plan's actions, open loop; the cost is the plan's own, less its terminal term; the plant is the plan's model over one step, linearised at the episodes' mean state and action, with the covariance of its one-step residuals on them as the noise. panel may be the one the plan was fitted on, and the plan was chosen on it, so a value read off it can be optimistic; a later one is not.

A window starts at its time zero, where a target trial starts each unit's follow-up: when the unit is eligible and is assigned a strategy (Hernán and Robins, Causal Inference: What If, section 22.4). Under "calendar", the default, the time zeros are the panel's periods H, 2H, ... before its last, the same for every unit, and a unit gives every window it was observed throughout. Under "unit" each unit's windows are cut back from its own latest period, a gap ending a run: where a unit's record ends then sets where each of its windows starts, so when a unit leaves the panel because of what happened to it, its windows are chosen by their own outcomes, and the value read off them is biased.

One unit's windows follow each other, dependent through its state, so the intervals come from n_resamples draws of the units with replacement, from seed, every window of a drawn unit kept. Each draw runs the evaluation again: the plant is linearised again, the logger fitted again unless it is given, the smoothing chosen again unless it is given, and the certificate and the values computed again. An interval is its estimate plus or minus 1.96 standard deviations of its draws, widened by the model's correction as evaluate_plan widens its own; a draw the certificate refuses is left out and counted (PlanEvaluation.bootstrap). With few units the draws are few distinct panels and the intervals too narrow.

Near the certificate's edge a draw whose units start wider or weigh heavier than the panel's is refused, so the draws read are the narrow side of the spread: the intervals are then conditional on certification, and reading says so, "conditional". Where more than max_refused of the draws are refused it is "inconclusive", logged as a warning: the intervals are too narrow to read. accepted and accepted_share count the draws read.

Windows of one unit leave nothing to resample, and they follow each other through its state and its noise. Each window's influence on the estimate, the delta method's, is read across the windows' time zeros by the equal-weighted cosine estimate, nu cosines of E windows, nu the most whole ones at or below 0.4 E^(2/3), and each interval is Student's t(nu) on it (Lazarus, Lewis, Stock and Watson 2018), widened by the model's correction; PlanEvaluation.degrees_of_freedom is nu and bootstrap is None. A window's time zero takes its place on the grid of H periods, so a gap leaves its places empty. The logger and the smoothing are held at the estimate's. Measured on one simulated unit (a loop keeping 0.5 or 0.8 of its state a step, its noise AR(0), AR(0.5) or AR(0.9), horizons 4 and 10, 25 to 1600 windows, 1000 logs a cell), the 95 % intervals covered 0.87 to 0.99, where the windows taken as independent covered 0.75 to 0.99; where the loop keeps 0.8 and the noise is AR(0.9), 0.82 to 0.95 against 0.64 to 0.85. They were 1.04 to 2.3 times as wide, 1.66 at 25 windows of fresh noise (ADR 0083).

PlanEvaluation.versus_logger holds the plan's value less the logged policy's, on the same windows, its interval read off the difference within each draw. The policy in place can cost less than the plan; the difference is what switching to the plan would change.

Scope: what chc.evaluation states, and two things more. The logs' actions must depend on the state and a randomisation of their own alone, so a plan whose levers were logged on a column outside its state is refused: the weights need the logger's propensity given that column, and no policy of the state is that. The graph says what the levers were logged on, their parents; an asserted adjustment is taken to name it. A covariate adjusted for only because it moves the target is no reason to refuse. And a window is read only when its unit was observed to its end. A unit that leaves the panel because of what happened to it after a window's time zero takes that window with it, and the windows left are not those an unselected panel would hold; weighting them by the chance of staying, which would correct that, is not built. Leaving on what happened before a time zero moves only the law the windows start from, which the value is read at.

What the graph cannot say, the panel is asked: whether the levers read a column besides the state, or the state's past (PlanEvaluation.logger_check, as Prescription.logger_check on the panel the plan was fitted on). On enough rows it also flags levers that read the state through more than a quadratic, such as a logger that switches at a threshold; the logger fitted here is affine, so that premise fails too. A rejection is logged as a warning and changes nothing else. Experimental.

Raises:

Type Description
NotIdentifiedError

if the effect is not identified, so there is no plan.

DecisionError

on a time_zero other than "calendar" and "unit"; fewer than two resamples; a max_refused outside [0, 1); a plan made against driver forecasts, whose plant changes with the step; a plan with a lever that follows its logged rule of the state, which no open-loop schedule carries; a plan whose levers were logged on a column outside its state, or whose record does not say; or a panel with fewer than two windows.

InfeasibleEvaluation

when the evaluation's certificate refuses, on the panel or on every draw but one.

PanelError

when a state's or a lever's column of panel does not hold real numbers, as prescribe refuses it.

reach()

Per lever, how far it can move the target's rate across its own box, where the plan starts; None where the log does not identify the lever's own effect.

The fitted control channel on the target's row, read at the state the plan starts from, or would have where the effect is not identified, times the width of the lever's box. A channel affine in the state has no one value: up to 0.12 this read its constant term, its value at x = 0, which a log of rooms at 20 °C never comes near, and a fit whose channel was negative everywhere in the log read positive there. A large coefficient on a lever that may barely move is not a large lever, which is why the range is in the number and not only in the footnote.

A lever the log set from the state, or moved only together with others, reads None. Its entry of the channel there leans on a direction the log never moved (chc.dynamics_id.CausalDynamicsFit.unmoved), where the fit holds the channel by a convention, not an estimate: on a log whose second lever was always twice the first, up to 0.14 this read 2.05 and 1.02, a split of their joint effect that any other split fits as well.

Read from the same fit that produced the plan, deliberately. Ranking by a second estimator --- local projections, say --- invites an ordering that contradicts the schedule printed beside it, and two disagreeing orderings on one page is worse than one.

Raises:

Type Description
ValueError

on a prescription that records no start, one built other than by prescribe without a plan; or on one whose fit left a direction unmoved and that does not record which levers the log identifies alone, one built other than by prescribe.

explain()

The levers ranked by reach, widest first, as text, and those whose own effect the log does not identify, by name.

report()

A Markdown summary: the decision, both certificate axes, and the provenance.

Text only, from string templates. No plotting dependency, so the output is deterministic and can be asserted on in a test rather than eyeballed.

to_json()

The schedule, both certificate axes and the provenance, as plain JSON-safe values.

No number is infinite or nan: one that is not finite is None (ADR 0055). Where None has more than one reading, a status beside it says which: regret_status refused for an infinite regret_bound, a selection step's own regret_status for its bound, gamma_star_status for gamma_star (GammaStarStatus), and stationarity_status and violation_status for stationarity and constraint_violation (chc.plan.DiagnosticStatus).

prescribe(panel, *, levers, target, horizon, adjustment, constraints=(), hold_constraints=False, budgets=(), max_levers=None, known=None, dt=1.0, gamma=1.0, tolerance=None, x0=None, folds=2, seed=0, integrator='rk4', drivers=())

Fit the causal control channel from panel and plan a certified schedule on it.

Parameters:

Name Type Description Default
panel Panel

long-format logs. Consecutive periods within a unit become the (x, u, x_next) transitions the channel is fitted on; gaps are dropped rather than interpolated, so an unbalanced panel is fine and a silently invented row is not. The channel's standard error, and with it the tube's budget, sums the scores within each group of the panel's declared cluster, or of its unit where it declares none, before squaring them (clusters in fit_causal_residual): the transitions of one unit share whatever persistent noise the model leaves out. It sums them within each period as well, and the error is the largest of the groups' and the periods' sums together and of each alone (ADR 0061): a shock every unit shares in a period, met by levers the units move together, makes the transitions of one period move together across units, and summed by unit alone the error read 0.29 to 0.58 of the estimate's spread. A panel whose transitions all fall in one group, one unit's or one declared cluster's, has no second group: it sums its transitions' scores within each period and reads the sums' dependence across the periods by cosines of them (periods in fit_causal_residual, ADR 0075), and where they all start in one period as well, takes them as independent. One whose transitions all start in one period sums within groups alone. The certificate says which it was (DecisionCertificate.error_clustered_by, error_periods, error_cosines).

required
levers, target, constraints

the decision, in the domain's own names. States are the target column followed by each other constrained column, in that order. A constraint may name the target column itself: it then bounds the steered state, which gets the barrier and gamma_star like any other constrained column, and adds no state. A lever's cap_per_step is held by every iterate of the solve; the regret bound stays priced against the box alone, so with a rate limit it is conservative rather than tight.

required
hold_constraints bool

hold constraints inside the solve, as the barrier condition chc.plan.certify_safety audits at gamma (chc.plan.BarrierConstraint), rather than only price the plan against them. Off by default, so no existing schedule moves; the certificate reads the same audit either way, and it is still the verdict -- a solve stopped by its budget can come back short of the condition. Each bound's condition is held on its own, as the audit reads it. The regret bound then includes what holding cost, since it is still priced against the box alone.

False
budgets Sequence[PeriodBudget]

what the plan may spend (chc.mpc.PeriodBudget): at most amount in each period steps from the plan's first, a step spending weights @ u, with each lever's spend per unit in the levers' order. A period the horizon cuts short gets its share of amount, so a budget for the whole horizon has period=horizon; the rows are chc.mpc.RecedingHorizon's at its first step, with nothing spent, and every iterate of the solve holds them. Prescription.budget_prices says what each period's budget is worth to the plan; the regret bound stays priced against the box alone, as with a rate limit.

()
max_levers int | None

plan with at most this many levers, chosen by greedy forward selection with chc.plan.causal_plan as its inner loop. Starting from no lever, each step plans once per lever not yet chosen, with that lever added, and keeps the cheapest plan --- under hold_constraints, the one the audit clears over the longest prefix first. Greedy is not exhaustive and can miss the best set; docs/adr/0004-greedy-lever-selection.md shows where. An unselected lever is held at zero at every step: the level at which the fitted control-affine channel credits it with no effect and its unit_cost charges nothing, and the level InterventionSchedule.windows reads as inactive. So every lever's box must contain zero. A lever the log never moved is no candidate, and stays on what the log did (below). The steps, each with its planned cost and regret bound, are Prescription.selection; the certificate's regret bound stays priced against every lever's box, so it includes what leaving levers out cost. None plans with every lever, as before; a value at or above the number of levers selects them all and returns that same plan.

None
adjustment CausalGraph | Sequence[str]

a CausalGraph to derive the adjustment set from, or a sequence of column names to assert it. Required, and deliberately so --- omitting it would default to adjusting for nothing, which is a causal claim, not an absence of one.

required
known Dynamics | None

physics kept fixed and not estimated. None means nothing is known and the whole vector field is the fitted residual.

None
dt float

the time step one panel period represents.

1.0
gamma float

the marginal sensitivity model's level the barrier is priced at (§40), passed to chc.plan.certify_safety with its default gap, the fitted channel's own size, and each lever's largest action as u_max, so gamma_star and the barrier's certified steps read the same in any units of the state or of each lever (ADR 0085, ADR 0091). It prices the plan, and changes it only under hold_constraints.

1.0
tolerance float | None

the trajectory error above which the plan stops being certified. Omitting it switches the tube off rather than setting it to infinity: this library cannot know how much error a caller accepts, and a certificate with an infinite tolerance passes over the whole horizon while proving nothing. The tube carries two errors over the plan's RK4 steps at the rate DecisionCertificate.tube_rate names: the channel's standard error, each lever's share of it times the largest distance that lever's box allows from the log's mean action, where the fit's drift absorbed the channel, root mean over the channel's entries, which reads the same in any units of each lever (ADR 0087) and wherever its zero is (ADR 0091), a scale and not a bound; and the plant's own noise, the plan's one-step error on the log, read off the fit's defect with its lag-1 correlation within units, at the radius that holds it at each step with probability 0.95 where the noise is Gaussian and AR(1) (ADR 0080, DecisionCertificate.tube_noise). Up to 0.16.0 the tube carried the channel's error alone: on 600 one-unit logs shaped like the BOPTEST case's it read 0.051 K one step ahead, where the plant's error one step ahead reached 0.16 to 0.22 K at 95 %; it held the plant at the first step on 40 % of draws and trusted 6 to 13 steps on nine logs in ten. It now holds the plant at the first step on 97 % to 99 % of draws and trusts 1 or 2 steps, where the plant's error stays within the tolerance of 0.5 K at 95 % for 2 or 3. A tube whose rate, budget or noise comes out other than a finite number is not evaluated, and a warning says so. The tube is one norm over the states, and tolerance one number in their units: states in units unlike each other mix in it, as the levers' once did. To give each state its own scale, log it in units of the error you accept on it, its target, its bounds and the prices with it, which the plan reads the same (ADR 0059), and pass tolerance=1 (ADR 0091).

None
x0 ArrayLike | None

the state to plan from, one finite value per state: the target's, then each constrained column's. Defaults to the mean over units of each unit's last observed state, the pooled "where we are now". Prescription.start records which, and for the panel's, over how many units, at which periods and with what spread.

None
integrator Integrator

the one-step map the fit is made consistent with, passed to fit_causal_residual. Defaults to "rk4" here and to "euler" there, deliberately: the low-level fit has no idea what will consume it and keeps its contract, while this function knows the field goes straight to chc.plan.causal_plan, which rolls out with RK4. Fitting one integrator and planning with another is a bias the caller never asked for --- on the plant in chc.mmm it costs 28% of the control channel. Choose "euler" when the panel is genuinely discrete-time (a weekly budget is not a sample of an ODE) and the one-step map is the model.

'rk4'
drivers Sequence[Driver]

exogenous columns the plan can forecast but not move, each with its forecast over the horizon (Driver). They are fitted into the drift jointly with the states (fit_causal_residual), and the plan is made against the forecast, so it acts ahead of a push it can see coming. The forecast moves the drift and never the channel, so gamma_star is priced with it in place. Two things are not priced: the forecast's own error, and the fitted gain's standard error, which the tube's model error leaves out as it leaves out the drift's. A forecast outside the range the panel logged is extrapolated by the fitted gain, and logged as a warning.

()

Returns:

Name Type Description
A Prescription

class:Prescription. Read DecisionCertificate.identification before

Prescription

attr:Prescription.schedule, which raises when the effect is not identified.

Prescription

attr:Prescription.run records what the run used (RunProvenance).

Raises:

Type Description
DecisionError

the decision is mis-specified --- no lever, a lever named twice or also the target or a constraint, a column constrained twice, a target schedule whose length is not horizon, constraints to hold with none given, max_levers below one or with a lever whose box excludes zero, a driver that is also a lever or a state or is named twice, a forecast that is not horizon + 1 finite levels, a budget that does not weigh one spend per lever, starts its periods anywhere but the plan's first step, or allows less than the levers' boxes spend at the least, a dt that is not a finite positive step, a tolerance that is negative or nan, a gamma below 1 or not finite, which is not a sensitivity level, a panel with no consecutive pair of periods to fit a transition on, a cap_per_step on a lever that follows its logged rule or a budget that prices one, max_levers where the log kept a combination of the levers away from zero, an x0 that is not one finite value per state, an asserted adjustment set that names a lever, or a covariate or a driver whose column would be read in a second role: one named u or x_next, or x other than as the one state, under which the transitions hold the levers, the states a period on and the states, or one named for a driver's level a period on, f"{driver}_next".

KeyError

a lever, target, constraint, driver or asserted covariate names a column the panel does not have. The message lists the panel's columns.

PanelError

a column the fit reads as numbers --- a lever's, a state's, a covariate's or a driver's --- holds text, dates, times, durations, complex numbers or any other value that is not a real number, or a number finite in its own type that is an infinity in float64, as chc.panel.Panel.wide refuses it. The message names the column, the value, its unit and its time.

A direction of the channel the log never moves apart from what the states and the covariates predict (unmoved) is not identified on this log, and the plan keeps to what the log did along it (ADR 0054). A lever whose whole channel is so is held at its mean logged level, clipped to its box, where the log kept it at one level, and follows the log's least-squares rule of the state inside the plan's field where the log set it from the state alone; DecisionCertificate.unmoved_levers and rule_levers name them, and the schedule's column carries the rule read along the predicted path. Among the other levers, a combination the log kept at one level is held there by an equality row at every step (relations). A lever or a combination the log set from anything else gives no plan, and neither does a log that moves no lever: the identification is then not_identified, its reason naming the levers and the columns they were set from. The plan is checked exactly: the fits the log cannot tell apart differ along directions in which the field is linear, and first_loaded_step is the first step at one of whose RK4 stages any of them moves the field, where the trustworthy prefix ends. estimability says which case holds.

Each decision point emits one logging record on chc.decision, keyed by chc_event (see _log). Nothing is configured here; a caller that wants them calls logging.basicConfig itself. An unidentified effect, a lever the log never moved, one on its logged rule, a combination kept at its logged level, a plan the log does not determine, a run in single precision, a forecast outside the logged range and levers that read more than the record says come through at WARNING.

Before fitting, the panel is asked whether the levers read anything but the states and their recorded parents: Prescription.logger_check (experimental). It reports; it changes nothing else.