| Title: | Bayesian Decision-Theoretic Actions under Loss and Hard Constraints |
|---|---|
| Description: | Maps a posterior distribution or an 'orchestra_manifest' inference result to an optimal action under an explicit loss function and a hard feasibility constraint, returning the action together with a grounding label and an abstention when evidence is insufficient. The grounding label is combined worst-case across inputs, so a decision resting on an unverified external fact is downgraded to abstention rather than asserted with false confidence. A manifest-native tail closes an analytics pipeline by deciding directly from an upstream member's inference result, and a grade-band loss prices quality-threshold ('protein-band') payoffs as a first-class step value schedule. Reference decisions cover bounded input-rate selection and constraint-capped position sizing. |
| Authors: | Max Moldovan [aut, cre] (ORCID: <https://orcid.org/0000-0001-9680-8474>) |
| Maintainer: | Max Moldovan <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.1.9000 |
| Built: | 2026-10-15 23:24:02 UTC |
| Source: | https://github.com/max578/decideR |
Coerce a CRRDC benefit-cost ratio to a data.frame
## S3 method for class 'decider_bcr' as.data.frame(x, ...)## S3 method for class 'decider_bcr' as.data.frame(x, ...)
x |
A |
... |
Ignored, for compatibility with |
The rates data.frame: one row per discount rate, columns
rate_label, rate, pv_benefits, pv_costs, npv, bcr.
bens <- data.frame(year = 2020:2021, value = c(0, 210)) csts <- data.frame(year = 2020:2021, value = c(100, 0)) up <- data.frame(year = 2020:2021, uptake = c(1, 1)) as.data.frame(bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no intervention", attribution = 1, adoption = up, grounding = grounding_grounded()))bens <- data.frame(year = 2020:2021, value = c(0, 210)) csts <- data.frame(year = 2020:2021, value = c(100, 0)) up <- data.frame(year = 2020:2021, uptake = c(1, 1)) as.data.frame(bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no intervention", attribution = 1, adoption = up, grounding = grounding_grounded()))
Discounts a benefit stream and a cost stream to a benefit-cost ratio in the
shape a Council of Rural Research and Development Corporations (CRRDC)
impact assessment reports, with the four inputs the guidelines make
mandatory: an explicit counterfactual, an explicit attribution share, an
adoption profile with lags, and a discount-rate sensitivity band around the
central rate. Where decide_bcr() leaves those assumptions to the caller,
bcr_crrdc() refuses to construct a ratio without all four – a
benefit-cost ratio that implicitly assumes no counterfactual, full
attribution and instant 100% uptake is exactly what the guidelines forbid.
bcr_crrdc( benefits, costs, counterfactual, attribution, adoption, counterfactual_label = NULL, discount_central = 0.05, discount_sensitivity = c(0.03, 0.07), base_year = NULL, grounding = NULL )bcr_crrdc( benefits, costs, counterfactual, attribution, adoption, counterfactual_label = NULL, discount_central = 0.05, discount_sensitivity = c(0.03, 0.07), base_year = NULL, grounding = NULL )
benefits |
A |
costs |
A |
counterfactual |
Required, no default. Either a |
attribution |
Required, no default. A single number in |
adoption |
Required, no default. A |
counterfactual_label |
A single character string naming the declared
counterfactual. Required when |
discount_central |
The central real discount rate, as a proportion
(CRRDC's Australian rural R&D standard is |
discount_sensitivity |
A numeric vector of sensitivity discount rates
reported alongside |
base_year |
The undiscounted period-zero year; defaults to the
earliest year across |
grounding |
Optional grounding token(s) for the input facts,
combined worst-case with |
counterfactual states what would have happened without the investment.
Supply a data.frame(year, value) of benefits under the counterfactual
scenario, spanning every year benefits covers, and it is subtracted from
benefits year by year before attribution and adoption are applied.
Alternatively, when benefits already IS the incremental (net-of-
counterfactual) stream, pass counterfactual = "already_net" and name the
declared counterfactual in counterfactual_label – the label is required
in this branch because a net stream carries no other record of what it is
net of.
attribution is the proportion, in (0, 1], of the incremental benefit
this investment alone caused, net of co-funders, extension effort and
other contributing causes. adoption is the uptake profile, a
data.frame(year, uptake) with uptake in [0, 1], applied
multiplicatively to the net, attributed benefit stream; it must cover
every year benefits covers, so an adoption lag is always stated rather
than assumed away. Costs are not adjusted by attribution or adoption –
the investment cost is what was actually spent, not a share of a benefit.
The ratio is reported at discount_central (the CRRDC standard is 5% for
Australian rural R&D) and, side by side, at every rate in
discount_sensitivity (the CRRDC standard band is 3% and 7%), so a reader
sees how sensitive the ratio is to the one assumption a guideline mandates
testing. base_year (period t = 0, undiscounted) defaults to the
earliest year across benefits and costs, so an investment whose costs
precede its first benefit year is discounted from the year spending began.
Grounding rides the same Independent Oracle Principle firewall as the rest
of decideR: an un-grounded input (the default when grounding is not
supplied) returns an abstained decider_bcr with abstain_reason = "input_ungrounded" and every rate's pv_benefits/pv_costs/npv/bcr
NA, never a ratio a funding submission would quote. A discounted cost
base that is not strictly positive at the central rate (a zero or
negative cost stream) abstains with abstain_reason = "undefined_ratio"
for the same reason.
A decider_bcr object: rates (a data.frame with one row per
discount rate and columns rate_label, rate, pv_benefits,
pv_costs, npv, bcr), counterfactual_label, attribution,
adoption, base_year, grounding, abstained and abstain_reason.
decide_bcr() for a benefit-cost ratio without the CRRDC
mandatory inputs.
Other value:
decide_bcr(),
decide_evpi(),
decide_evsi(),
valuation()
# A ten-year, evenly-adopted, half-attributed investment. The benefit # stream is already declared incremental against a named counterfactual. bens <- data.frame(year = 2020:2029, value = rep(500, 10)) csts <- data.frame(year = 2020:2029, value = rep(1000, 10)) up <- data.frame(year = 2020:2029, uptake = rep(1, 10)) bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no new variety released", attribution = 0.5, adoption = up, grounding = grounding_grounded())# A ten-year, evenly-adopted, half-attributed investment. The benefit # stream is already declared incremental against a named counterfactual. bens <- data.frame(year = 2020:2029, value = rep(500, 10)) csts <- data.frame(year = 2020:2029, value = rep(1000, 10)) up <- data.frame(year = 2020:2029, uptake = rep(1, 10)) bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no new variety released", attribution = 0.5, adoption = up, grounding = grounding_grounded())
A decision that rests on several inputs is grounded only when every input is grounded; a single un-grounded input taints the whole decision. This is the worst-case combination mandated by the Independent Oracle Principle, and it is the rule by which a decision inherits its grounding label from the facts it consumes.
combine_grounding(tokens)combine_grounding(tokens)
tokens |
A character vector of grounding tokens, each either
|
Each token is validated against the canonical vocabulary, so a synonym such
as "unverified" is rejected loudly rather than silently mistaken for a
grounded fact. The token written by decideR 0.2.0 and earlier is accepted
as un-grounded.
grounding_grounded() if tokens is non-empty and every element is
grounded; otherwise grounding_unverified().
grounding_tokens for the two canonical tokens.
Other grounding:
grounding_tokens
combine_grounding(c(grounding_grounded(), grounding_grounded())) combine_grounding(c(grounding_grounded(), grounding_unverified()))combine_grounding(c(grounding_grounded(), grounding_grounded())) combine_grounding(c(grounding_grounded(), grounding_unverified()))
The shared decision rule. Given draws from the posterior of a latent state
, a utility function , and a set of candidate
actions, decide() evaluates the expected utility
of each feasible candidate by averaging over the
draws and selects the maximiser. It declines to do so – abstaining to
safe_action – under any of four conditions, checked in this order: the
combined input grounding is not grounded (the Independent Oracle Principle
firewall: never act on an unverified fact); no candidate satisfies
constraint; the effective sample size is below min_ess (this trigger is
opt-in – it never fires unless the caller supplies min_ess, since there is
no universal floor a decision-agnostic engine can default to); or the chosen
action beats the safe_action on fewer than decisive_prob of the draws
(it is not clearly better than the status quo). The last check is judged
against the safe action, not the runner-up, so a fine candidate grid does
not trigger spurious indecision; when safe_action has no numeric match in
candidates (a categorical action set, or an off-grid numeric safe action)
it falls back to the per-draw argmax probability, so the gate stays live
for every candidate shape rather than becoming a no-op.
decide( draws, utility, candidates, labels = NULL, constraint = NULL, grounding = grounding_unverified(), safe_action = NULL, safe_label = "abstain", min_ess = NULL, ess = NULL, decisive_prob = 0.6, method = "expected_utility", inputs = list(), metadata = list() )decide( draws, utility, candidates, labels = NULL, constraint = NULL, grounding = grounding_unverified(), safe_action = NULL, safe_label = "abstain", min_ess = NULL, ess = NULL, decisive_prob = 0.6, method = "expected_utility", inputs = list(), metadata = list() )
draws |
A numeric vector of posterior draws of the latent state. |
utility |
A function |
candidates |
A numeric vector (or list) of candidate actions. |
labels |
Optional character labels for the candidates; defaults to a
formatted |
constraint |
Optional |
grounding |
One or more grounding tokens for the inputs; combined worst-case. |
safe_action |
The action returned on abstention (e.g. a flat position, a zero input rate, the status quo). |
safe_label |
A label for the safe action. |
min_ess |
Optional effective-sample-size floor; the |
ess |
Optional effective sample size of |
decisive_prob |
The minimum posterior probability that the chosen action
beats |
method |
A label recording the decision rule. |
inputs |
A named list recording input provenance. |
metadata |
A named list of free-form metadata. |
The grounding label is combined worst-case across grounding with
combine_grounding(), so passing the provenance of several inputs taints the
decision if any one of them is unverified.
A decision object.
decide_input_rate() and decide_position_size() for domain
wrappers; decision for the returned object.
Other decisions:
decide_input_rate(),
decide_position_size(),
decision(),
is_grounded()
set.seed(1L) theta <- rnorm(2000L, mean = 1.2, sd = 0.4) u <- function(a, theta) -(a - theta)^2 decide(theta, u, candidates = seq(0, 3, by = 0.5), grounding = grounding_grounded(), safe_action = 0)set.seed(1L) theta <- rnorm(2000L, mean = 1.2, sd = 0.4) u <- function(a, theta) -(a - theta)^2 decide(theta, u, candidates = seq(0, 3, by = 0.5), grounding = grounding_grounded(), safe_action = 0)
Discounts a stream of benefits and a stream of costs at an explicit rate over an explicit horizon and reports the components an impact assessment is written from – the two streams, the present value of each, the net present value, and the ratio:
This is the CRRDC-style reporting set an Australian rural research and development impact assessment closes with. decideR encodes no guideline default for the rate or the horizon: both are stated by the assessment the analysis serves and are the caller's to supply, so a ratio can never be quoted against an assumption its author did not make.
decide_bcr( benefits = NULL, costs, discount_rate, horizon = NULL, grounding = NULL, manifest = NULL, draws_key = "benefit_draws", probs = c(0.05, 0.5, 0.95), metadata = list() )decide_bcr( benefits = NULL, costs, discount_rate, horizon = NULL, grounding = NULL, manifest = NULL, draws_key = "benefit_draws", probs = c(0.05, 0.5, 0.95), metadata = list() )
benefits |
The benefit stream: a numeric vector, one value per period
from |
costs |
The cost stream, a numeric vector, one value per period from
|
discount_rate |
The real discount rate per period, as a proportion
(e.g. |
horizon |
The number of periods, |
grounding |
Optional grounding token(s) for the streams, combined
worst-case; defaults to the manifest's own token, or to
|
manifest |
Optional |
draws_key |
The |
probs |
Quantiles at which the posterior ratio is summarised when the benefit stream arrives as draws. |
metadata |
A named list of free-form metadata. |
Period t = 0 is the present and is not discounted. A stream given as
end-of-period cash flows is expressed by leading it with a zero, as in the
worked example below. Streams shorter than the horizon are zero-padded;
a stream longer than the horizon is an error rather than a silent
truncation.
When manifest is supplied, the benefit stream and its grounding token are
read from it through the same duck-typed contract accessors the decision
tail uses (outputs, then metadata[[draws_key]]; grounding from
metadata$grounding or summary$metrics$grounding), with draws in rows and
periods in columns. The point estimate then uses the posterior mean stream,
and the posterior distribution of the ratio is summarised in
components$bcr_quantiles. An ungrounded manifest returns an abstained
valuation, never a ratio.
A valuation of type = "bcr" whose value is the ratio and whose
components carry the discounted streams, the two present values and the
net present value.
decide_evsi() for the value of the next study rather than of the
whole programme.
Other value:
bcr_crrdc(),
decide_evpi(),
decide_evsi(),
valuation()
# A two-period investment: 100 spent now, 210 returned next period, at a # five per cent discount rate. PV(costs) = 100, PV(benefits) = 210/1.05 = # 200, so NPV = 100 and BCR = 2. v <- decide_bcr(benefits = c(0, 210), costs = c(100, 0), discount_rate = 0.05, grounding = grounding_grounded()) v@value v@components$npv # The same investment on an un-grounded benefit stream prices out as an # abstention rather than as a ratio. decide_bcr(benefits = c(0, 210), costs = c(100, 0), discount_rate = 0.05)# A two-period investment: 100 spent now, 210 returned next period, at a # five per cent discount rate. PV(costs) = 100, PV(benefits) = 210/1.05 = # 200, so NPV = 100 and BCR = 2. v <- decide_bcr(benefits = c(0, 210), costs = c(100, 0), discount_rate = 0.05, grounding = grounding_grounded()) v@value v@components$npv # The same investment on an un-grounded benefit stream prices out as an # abstention rather than as a ratio. decide_bcr(benefits = c(0, 210), costs = c(100, 0), discount_rate = 0.05)
Prices what it would be worth to learn the latent state exactly before
acting. Given the same manifest, candidate set and utility that
decide_from_manifest() takes, decide_evpi() returns
the difference between deciding with the state known and deciding with only its posterior. It is the ceiling on the value of any study, trial or measurement addressing that state: no finite sample can be worth more, which is what makes it the natural first screen before an experiment is designed.
decide_evpi( manifest, candidates, utility = NULL, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )decide_evpi( manifest, candidates, utility = NULL, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )
manifest |
An |
candidates |
A numeric vector (or list) of candidate actions. |
utility |
A function |
grounding |
Optional override for the manifest's own grounding token. |
safe_action |
The action the baseline decision abstains to; defaults to the first candidate. |
draws_key |
The |
... |
Further arguments passed to the decision core (e.g. |
The expectation runs over the feasible candidates only, so a hard constraint
passed through ... prices the information available within the actions
the grower or the agency can actually take, not within a wider hypothetical
set.
Grounding rides from the producer exactly as it does for a decision: an
ungrounded manifest returns an abstained valuation with
abstain_reason = "input_ungrounded" and value = NA_real_. The other
blocking reasons are no_feasible_action, low_ess and
degenerate_utility. An insufficient_evidence baseline decision is not
blocking: a too-close-to-call decision is precisely where information pays,
and the indecisive decision is returned in @baseline.
A valuation of type = "evpi". Its components carry the two
expectations the value is the difference of, the draw count and the number
of feasible candidates; @baseline carries the decision current evidence
supports.
Raiffa, H. and Schlaifer, R. (1961) Applied Statistical Decision Theory. Boston: Harvard University Graduate School of Business Administration.
decide_evsi() for the value of one finite sample;
decide_from_manifest() for the decision the value is measured against.
Other value:
bcr_crrdc(),
decide_bcr(),
decide_evsi(),
valuation()
set.seed(1L) rates <- seq(0, 200, by = 25) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(800L, 5.5, 0.6) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(800L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_evpi(m, candidates = rates, utility = function(r, y) 300 * y - 1.2 * r)set.seed(1L) rates <- seq(0, 200, by = 25) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(800L, 5.5, 0.6) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(800L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_evpi(m, candidates = rates, utility = function(r, y) 300 * y - 1.2 * r)
Prices a named, finite study before it is run: this soil test, this trial,
at this site. Where decide_evpi() prices perfect knowledge,
decide_evsi() prices the imperfect knowledge a real sample would deliver,
the expected gain from being allowed to decide after seeing the data rather than before. It is bounded above by the EVPI, which the valuation reports beside it as a ceiling.
decide_evsi( manifest, candidates, sample_design, utility = NULL, state = NULL, state_key = NULL, n_pre = 500L, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )decide_evsi( manifest, candidates, sample_design, utility = NULL, state = NULL, state_key = NULL, n_pre = 500L, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )
manifest |
An |
candidates |
A numeric vector (or list) of candidate actions. |
sample_design |
A named list describing the prospective sample:
|
utility |
A function |
state |
Optional numeric vector, one latent-state value per draw, that the prospective sample informs. |
state_key |
Optional name of a column of the manifest's |
n_pre |
The number of outer (pre-posterior) draws. |
grounding |
Optional override for the manifest's own grounding token. |
safe_action |
The action the baseline decision abstains to. |
draws_key |
The |
... |
Further arguments passed to the decision core (e.g. |
The pre-posterior expectation is evaluated by the standard two-loop Monte
Carlo algorithm: draw a latent state from the current posterior, simulate the
dataset the sample would produce at that state, update, and re-decide. The
inner update here is an importance re-weighting of the manifest's own draws
by the sampling log-likelihood rather than a nested re-fit, so the manifest
stays the single source of the posterior and decideR adds no sampler. That
re-weighting degenerates when the prospective sample is very much more
informative than the current posterior; the effective sample size of the
weights is therefore computed at every outer draw, reported in
components$weight_ess_min, and warned about when it collapses.
The estimate is a Monte Carlo one and is reported unclamped. A small negative value is Monte Carlo noise around a true value near zero – a sample worth nothing – and is not silently floored at zero.
A valuation of type = "evsi", whose components carry the
pre-posterior and current-information expectations, the EVPI ceiling, the
weight diagnostics, and – when sample_design$cost is supplied – the
cost and the net value.
Raiffa, H. and Schlaifer, R. (1961) Applied Statistical Decision Theory. Boston: Harvard University Graduate School of Business Administration.
Ades, A. E., Lu, G. and Claxton, K. (2004) Expected value of sample information calculations in medical decision modeling. Medical Decision Making, 24(2), 207–227. doi:10.1177/0272989X04263162
decide_evpi() for the ceiling on any sample's value.
Other value:
bcr_crrdc(),
decide_bcr(),
decide_evpi(),
valuation()
set.seed(2L) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) # A posterior for the yield gain from a nitrogen top-up, in t/ha. gain <- rnorm(600L, mean = 0.10, sd = 0.35) m <- demo_manifest( outputs = matrix(gain, ncol = 1L), metadata = list(grounding = grounding_grounded()) ) # A four-plot strip trial measuring the gain, observation sd 0.5. trial <- list( label = "4-plot strip trial", n_obs = 4L, cost = 0.02, simulate = function(state) mean(rnorm(4L, state, 0.5)), loglik = function(y, state) dnorm(y, state, 0.5 / sqrt(4), log = TRUE) ) decide_evsi(m, candidates = c(0, 1), sample_design = trial, utility = function(a, g) a * g, n_pre = 200L)set.seed(2L) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) # A posterior for the yield gain from a nitrogen top-up, in t/ha. gain <- rnorm(600L, mean = 0.10, sd = 0.35) m <- demo_manifest( outputs = matrix(gain, ncol = 1L), metadata = list(grounding = grounding_grounded()) ) # A four-plot strip trial measuring the gain, observation sd 0.5. trial <- list( label = "4-plot strip trial", n_obs = 4L, cost = 0.02, simulate = function(state) mean(rnorm(4L, state, 0.5)), loglik = function(y, state) dnorm(y, state, 0.5 / sqrt(4), log = TRUE) ) decide_evsi(m, candidates = c(0, 1), sample_design = trial, utility = function(a, g) a * g, n_pre = 200L)
The manifest-native pipeline tail. Given an orchestra_manifest emitted by
any ORCHESTRA member – a PESTO ensemble inversion, a kernR causal test, a
flexyBayes posterior – decide_from_manifest() extracts the predictive
draws and the producer's grounding token from the manifest, then routes them
through the same expected-utility engine decide() uses. It is the standard
closer of the crop pipeline: an upstream member's inference becomes a
loss-optimal, grounding-aware action without the caller re-assembling the
draws by hand.
decide_from_manifest( manifest, candidates, utility = NULL, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )decide_from_manifest( manifest, candidates, utility = NULL, grounding = NULL, safe_action = NULL, draws_key = "yield_draws", ... )
manifest |
An |
candidates |
A numeric vector of candidate actions, one per column of
the manifest's draws matrix; or a numeric vector the |
utility |
A function |
grounding |
Optional override for the manifest's own grounding token; defaults to the token the manifest carries, read worst-case. |
safe_action |
The action returned on abstention; defaults to the first candidate (the status-quo convention for an ordered grid). |
draws_key |
The |
... |
Further arguments passed to the decision core (e.g. |
The grounding label rides from the producer through the decision: a manifest
whose payload is ungrounded forces an abstention to safe_action,
regardless of how attractive the draws look. This is the Independent Oracle
Principle firewall applied at the pipeline boundary – decideR never mints a
confident recommendation on an upstream fact the producer did not ground.
decideR does not depend on the contract package; the manifest is read through
its public S7 properties (outputs, metadata, summary). The draws are
sought in the outputs slot first, then under metadata[[draws_key]], so the
tail consumes any conforming producer rather than special-casing each one.
A decision object whose inputs record the manifest's run_id and
emitter_package for provenance.
decide_rate_from_manifest() for the agronomic convenience;
decide() for the underlying engine.
Other manifest:
decide_rate_from_manifest()
set.seed(1L) rates <- seq(0, 200, by = 25) # In production the manifest is a real `orchestra_manifest` S7 object from # the composition layer. Here, a minimal S7 stand-in carrying the same # public properties the tail reads (`outputs`, `metadata`, `summary`, # `run_id`, `emitter_package`) -- an S3 list is NOT readable by the tail, # which duck-types through S7 property access. demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(1500L, 5.5, 0.3) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(1500L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_from_manifest(m, candidates = rates, utility = function(r, y) 300 * y - 1.2 * r)set.seed(1L) rates <- seq(0, 200, by = 25) # In production the manifest is a real `orchestra_manifest` S7 object from # the composition layer. Here, a minimal S7 stand-in carrying the same # public properties the tail reads (`outputs`, `metadata`, `summary`, # `run_id`, `emitter_package`) -- an S3 list is NOT readable by the tail, # which duck-types through S7 property access. demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(1500L, 5.5, 0.3) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(1500L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_from_manifest(m, candidates = rates, utility = function(r, y) 300 * y - 1.2 * r)
A reference decision for the agronomic domain. Given posterior draws of yield
at each candidate input rate – the kind of object a mechanistic-simulator
inversion (PESTO) or a causal test (kernR) emits – it selects the rate that
maximises expected profit, , where is the
grain price and the input price. It abstains to safe_rate when the
yield evidence is un-grounded or the leading rate is not decisive.
decide_input_rate( yield_draws, rates, price_grain, price_input, grounding = grounding_unverified(), safe_rate = 0, decisive_prob = 0.6, ... )decide_input_rate( yield_draws, rates, price_grain, price_input, grounding = grounding_unverified(), safe_rate = 0, decisive_prob = 0.6, ... )
yield_draws |
A numeric matrix of posterior yield draws, one column per candidate rate (rows are draws). |
rates |
A numeric vector of candidate input rates, one per column of
|
price_grain |
Grain price per unit yield. |
price_input |
Input price per unit rate. |
grounding |
One or more grounding tokens for the yield evidence; combined worst-case. |
safe_rate |
The fallback rate on abstention (default |
decisive_prob |
Minimum posterior probability that the leading rate is
best (see |
... |
Further arguments passed to the decision core: |
A decision object whose action is the chosen rate.
decide() for the underlying engine; decide_position_size() for
the trading counterpart.
Other decisions:
decide(),
decide_position_size(),
decision(),
is_grounded()
set.seed(1L) rates <- seq(0, 200, by = 40) # diminishing-returns yield response with posterior noise mu <- 3 + 2.5 * (1 - exp(-rates / 80)) yield_draws <- sapply(mu, function(m) rnorm(1500L, m, 0.3)) decide_input_rate(yield_draws, rates, price_grain = 300, price_input = 1.2, grounding = grounding_grounded())set.seed(1L) rates <- seq(0, 200, by = 40) # diminishing-returns yield response with posterior noise mu <- 3 + 2.5 * (1 - exp(-rates / 80)) yield_draws <- sapply(mu, function(m) rnorm(1500L, m, 0.3)) decide_input_rate(yield_draws, rates, price_grain = 300, price_input = 1.2, grounding = grounding_grounded())
A reference decision for the trading domain. Given predictive draws of the
next-period simple return, it chooses the fraction of capital that maximises
expected log-growth (the Kelly objective; Kelly, 1956)
over a grid
bounded by kelly_cap, subject to a hard one-period loss-quantile
constraint: a fraction is feasible only if its loss_quantile return
quantile is no worse than -max_drawdown. This guard is a single-period
value-at-risk floor, not a path drawdown – max_drawdown names the worst
loss_quantile-fraction of next-period returns the position may be
exposed to, not a peak-to-trough statistic accumulated over a trading path;
a true multi-period drawdown limit is not computed here. With no edge the
grid maximiser is naturally flat; with an edge the constraint caps the size.
It abstains to flat (0) when the return evidence is un-grounded – the
capital firewall: never stake real money on a market fact that has not been
verified against the exchange.
decide_position_size( return_draws, capital, max_drawdown, kelly_cap = 0.5, n_grid = 41L, loss_quantile = 0.05, grounding = grounding_unverified(), decisive_prob = 0.6, ... )decide_position_size( return_draws, capital, max_drawdown, kelly_cap = 0.5, n_grid = 41L, loss_quantile = 0.05, grounding = grounding_unverified(), decisive_prob = 0.6, ... )
return_draws |
A numeric vector of predictive next-period simple returns. |
capital |
The capital available; the dollar position is
|
max_drawdown |
The hard one-period loss-quantile cap (e.g. |
kelly_cap |
The maximum fraction of capital (a fractional-Kelly ceiling). |
n_grid |
The number of fractions evaluated on |
loss_quantile |
The lower return quantile the |
grounding |
One or more grounding tokens for the return evidence; combined worst-case. |
decisive_prob |
Minimum posterior probability that the leading fraction
is best (see |
... |
Further arguments passed to the decision core: |
A decision object whose action is the chosen fraction of capital;
the implied dollar position is in @metadata$position.
Kelly, J. L. (1956). A New Interpretation of Information Rate. Bell System Technical Journal, 35(4), 917-926.
decide() for the underlying engine; decide_input_rate() for the
agronomic counterpart.
Other decisions:
decide(),
decide_input_rate(),
decision(),
is_grounded()
set.seed(1L) # a small positive edge with fat tails r <- 0.004 + 0.02 * rt(4000L, df = 4) / sqrt(2) decide_position_size(r, capital = 1000, max_drawdown = 0.15, grounding = grounding_grounded())set.seed(1L) # a small positive edge with fat tails r <- 0.004 + 0.02 * rt(4000L, df = 4) / sqrt(2) decide_position_size(r, capital = 1000, max_drawdown = 0.15, grounding = grounding_grounded())
The agronomic manifest tail. Where decide_input_rate() takes a yield-draws
matrix assembled by the caller, decide_rate_from_manifest() sources that
matrix – and the grounding token – directly from an orchestra_manifest
emitted upstream (a PESTO inversion or a mechanistic yield-response posterior). It is
the formalised crop-pipeline tail: a yield posterior in, a profit-optimal,
grounding-aware nitrogen-rate decision out. The grounding rides from the
producer, so an un-grounded yield posterior forces the status-quo rate.
decide_rate_from_manifest( manifest, rates, price_grain, price_input, safe_rate = 0, decisive_prob = 0.6, ... )decide_rate_from_manifest( manifest, rates, price_grain, price_input, safe_rate = 0, decisive_prob = 0.6, ... )
manifest |
An |
rates |
A numeric vector of candidate input rates, one per column of the manifest's draws matrix. |
price_grain |
Grain price per unit yield. |
price_input |
Input price per unit rate. |
safe_rate |
The fallback rate on abstention (default |
decisive_prob |
Minimum posterior probability that the leading rate is
best (see |
... |
Further arguments passed to the decision core (e.g. |
A decision object whose action is the chosen rate.
decide_from_manifest() for the generic tail;
decide_input_rate() for the caller-assembled-matrix form.
Other manifest:
decide_from_manifest()
set.seed(1L) rates <- seq(0, 200, by = 25) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(1500L, 5.5, 0.3) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(1500L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_rate_from_manifest(m, rates, price_grain = 300, price_input = 1.2)set.seed(1L) rates <- seq(0, 200, by = 25) demo_manifest <- S7::new_class( "demo_manifest", properties = list( outputs = S7::new_property(S7::class_any, default = NULL), metadata = S7::new_property(S7::class_list, default = list()), summary = S7::new_property(S7::class_any, default = NULL), run_id = S7::new_property(S7::class_character, default = ""), emitter_package = S7::new_property(S7::class_character, default = "") ) ) ymax <- rnorm(1500L, 5.5, 0.3) yld <- vapply(rates, function(r) 3 + (ymax - 3) * (1 - exp(-r / 80)), numeric(1500L)) m <- demo_manifest( metadata = list(yield_draws = yld, grounding = grounding_grounded()) ) decide_rate_from_manifest(m, rates, price_grain = 300, price_input = 1.2)
The object returned by decide() and the reference decisions. It records
the chosen action, the expected_utility that selected it, the complete
candidates ledger (every action with its expected utility and
feasibility), and the two provenance fields the Independent Oracle Principle
requires: grounding (grounding_grounded() or grounding_unverified())
and the abstention record (abstained, abstain_reason). When a decision
abstains the action is the caller's safe_action, not the utility-maximal
one.
decision( action = NULL, action_label = NA_character_, expected_utility = NA_real_, candidates = NULL, grounding = "ungrounded", abstained = FALSE, abstain_reason = NA_character_, safe_action = NULL, method = NA_character_, inputs = list(), metadata = list() )decision( action = NULL, action_label = NA_character_, expected_utility = NA_real_, candidates = NULL, grounding = "ungrounded", abstained = FALSE, abstain_reason = NA_character_, safe_action = NULL, method = NA_character_, inputs = list(), metadata = list() )
action |
The chosen action. The |
action_label |
A short human-readable label for the action; suffixed
|
expected_utility |
The expected (mean-over-draws) utility of the chosen
action, or |
candidates |
A |
grounding |
A canonical grounding token. |
abstained |
|
abstain_reason |
A short reason string, or |
safe_action |
The fallback action taken on abstention. |
method |
The decision rule applied (e.g. |
inputs |
A named list recording input provenance. |
metadata |
A named list of free-form metadata. |
A decision S7 object.
decide() and the reference decisions that produce this object.
Other decisions:
decide(),
decide_input_rate(),
decide_position_size(),
is_grounded()
d <- decision(action = 120, action_label = "rate=120 kg/ha", expected_utility = 1840, grounding = grounding_grounded()) d@actiond <- decision(action = 120, action_label = "rate=120 kg/ha", expected_utility = 1840, grounding = grounding_grounded()) d@action
Turns a grade_band_value schedule into a utility(action, theta) function
the decision engine consumes. theta is a posterior draw of the quality
outcome (grain protein, say); the action shifts that outcome through
quality_shift, a function function(action, theta) returning the realised
quality under the action. The utility of an action at a draw is the band
payoff of the shifted outcome, less an optional per-unit action cost. Because
the payoff is a step function, the expected utility decideR averages over the
draws is governed by each band's posterior probability – exactly the
threshold economics a quality-graded decision faces.
grade_band_utility(value, quality_shift = NULL, cost = 0)grade_band_utility(value, quality_shift = NULL, cost = 0)
value |
A grade_band_value schedule. |
quality_shift |
A function |
cost |
A function |
When quality_shift is left NULL the action is taken to be the quality
outcome offset added to theta (the common case where an input nudges the
outcome additively), so quality = theta + action. Supply a quality_shift
for any non-additive response (a saturating dose-response, say).
A function function(action, theta) suitable as the utility
argument to decide().
grade_band_value() for the schedule; decide() for the engine.
Other loss:
grade_band_value()
v <- grade_band_value(c(10.5, 11.5, 13), c(300, 330, 360, 400)) # A nitrogen rate lifts protein additively; protein costs nothing but N does. u <- grade_band_utility(v, quality_shift = function(rate, protein) protein + 0.01 * rate, cost = 1.2) set.seed(1L) protein <- rnorm(2000L, mean = 11.0, sd = 0.6) decide(protein, u, candidates = seq(0, 150, by = 25), grounding = grounding_grounded(), safe_action = 0)v <- grade_band_value(c(10.5, 11.5, 13), c(300, 330, 360, 400)) # A nitrogen rate lifts protein additively; protein costs nothing but N does. u <- grade_band_utility(v, quality_shift = function(rate, protein) protein + 0.01 * rate, cost = 1.2) set.seed(1L) protein <- rnorm(2000L, mean = 11.0, sd = 0.6) decide(protein, u, candidates = seq(0, 150, by = 25), grounding = grounding_grounded(), safe_action = 0)
Builds a step value schedule: a payoff that is constant within a quality band and jumps at each breakpoint. This is the canonical shape of a quality-graded payoff – a grain price that steps up as protein clears 10.5%, 11.5%, and 13%, say – where the value of an outcome depends on which band it falls in, not on its exact magnitude. The returned object is a vectorised function from a quality outcome to its band payoff, classed so it carries the schedule for inspection and prints as a readable band table.
grade_band_value(breaks, values, right = FALSE, labels = NULL)grade_band_value(breaks, values, right = FALSE, labels = NULL)
breaks |
A strictly increasing numeric vector of band edges (e.g. the
protein thresholds |
values |
A numeric vector of band payoffs, of length
|
right |
Logical; when [lower, upper) |
labels |
Optional character labels for the bands, of length
|
breaks are the band edges, strictly increasing. values are the payoffs,
one per band: the first applies below breaks[1], the last applies at or
above breaks[length(breaks)], and each interior element applies between a
consecutive pair of breaks. With right = FALSE (the default) each band is
the half-open interval [lower, upper), so an outcome exactly on a break
clears into the higher band – the convention a delivery standard uses when
it pays the better grade at the threshold.
A function of one numeric argument returning the band payoff for each
element, with class grade_band_value and the schedule stored as
attributes (breaks, values, labels, right).
grade_band_utility() to turn the schedule into a decide()
utility; decide() for the engine that consumes it.
Other loss:
grade_band_utility()
# An APW-style protein payoff: $/t steps up as protein clears each band. v <- grade_band_value(breaks = c(10.5, 11.5, 13), values = c(300, 330, 360, 400)) v(c(9.0, 11.0, 11.5, 14.0)) v# An APW-style protein payoff: $/t steps up as protein clears each band. v <- grade_band_value(breaks = c(10.5, 11.5, 13), values = c(300, 330, 360, 400)) v(c(9.0, 11.0, 11.5, 14.0)) v
The two states a fact – or a decision assembled from facts – can occupy
under the Independent Oracle Principle. grounding_grounded() returns the
token for a fact that has been checked against its external authority;
grounding_unverified() returns the token for everything else, which is the
default until a grounding check proves otherwise.
grounding_grounded() grounding_unverified()grounding_grounded() grounding_unverified()
grounding_unverified() returns "ungrounded". The token written by
decideR 0.2.0 and earlier is still accepted wherever a token is read.
A length-one character string holding the canonical token.
combine_grounding() for worst-case combination across inputs.
Other grounding:
combine_grounding()
grounding_grounded() grounding_unverified()grounding_grounded() grounding_unverified()
A convenience predicate reading the decision's grounding label. It reports
only the grounding of the evidence, not whether the decision abstained –
is_grounded() is TRUE for an abstained decision whenever the abstention
reason was no_feasible_action, low_ess, or insufficient_evidence,
since none of those reasons downgrades the grounding label itself. A
consumer that must refuse to act on either un-grounded evidence or an
abstained decision – for example a live trading order path – gates on
is_grounded(x) && !x@abstained, not on is_grounded() alone.
is_grounded(x)is_grounded(x)
x |
A |
TRUE when the decision's grounding is grounding_grounded().
This does not imply the decision was not abstained; see Details.
Other decisions:
decide(),
decide_input_rate(),
decide_position_size(),
decision()
is_grounded(decision(grounding = grounding_grounded())) is_grounded(decision(grounding = grounding_unverified()))is_grounded(decision(grounding = grounding_grounded())) is_grounded(decision(grounding = grounding_unverified()))
Print a decision
x |
A decision. |
... |
Ignored, for compatibility with |
x, invisibly.
print(decision(action_label = "flat", grounding = grounding_grounded()))print(decision(action_label = "flat", grounding = grounding_grounded()))
Print a valuation
x |
A valuation. |
... |
Ignored, for compatibility with |
x, invisibly.
print(valuation(value = 12.4, type = "evpi", grounding = grounding_grounded()))print(valuation(value = 12.4, type = "evpi", grounding = grounding_grounded()))
Print a CRRDC benefit-cost ratio
## S3 method for class 'decider_bcr' print(x, ...)## S3 method for class 'decider_bcr' print(x, ...)
x |
A |
... |
Ignored, for compatibility with |
x, invisibly.
bens <- data.frame(year = 2020:2021, value = c(0, 210)) csts <- data.frame(year = 2020:2021, value = c(100, 0)) up <- data.frame(year = 2020:2021, uptake = c(1, 1)) print(bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no intervention", attribution = 1, adoption = up, grounding = grounding_grounded()))bens <- data.frame(year = 2020:2021, value = c(0, 210)) csts <- data.frame(year = 2020:2021, value = c(100, 0)) up <- data.frame(year = 2020:2021, uptake = c(1, 1)) print(bcr_crrdc(bens, csts, counterfactual = "already_net", counterfactual_label = "no intervention", attribution = 1, adoption = up, grounding = grounding_grounded()))
Print a grade-band value schedule
## S3 method for class 'grade_band_value' print(x, ...)## S3 method for class 'grade_band_value' print(x, ...)
x |
A grade_band_value schedule. |
... |
Ignored, for compatibility with |
x, invisibly.
print(grade_band_value(c(10.5, 11.5), c(300, 330, 360)))print(grade_band_value(c(10.5, 11.5), c(300, 330, 360)))
The object returned by decide_evpi(), decide_evsi() and decide_bcr().
It records the priced value, the type of valuation that produced it, the
arithmetic components a reader needs in order to audit that value, and the
two Independent Oracle Principle provenance fields: grounding, and the
abstention record (abstained, abstain_reason). When a valuation abstains,
value is NA_real_ – decideR never mints a dollar figure on evidence its
producer did not ground.
valuation( value = NA_real_, type = NA_character_, components = list(), grounding = "ungrounded", abstained = FALSE, abstain_reason = NA_character_, baseline = NULL, method = NA_character_, inputs = list(), metadata = list() )valuation( value = NA_real_, type = NA_character_, components = list(), grounding = "ungrounded", abstained = FALSE, abstain_reason = NA_character_, baseline = NULL, method = NA_character_, inputs = list(), metadata = list() )
value |
The priced quantity, in the utility units of the inputs
(currency per unit area for a crop valuation, a dimensionless ratio for a
benefit-cost ratio), or |
type |
A short valuation type: |
components |
A named list of the parts the |
grounding |
A canonical grounding token. |
abstained |
|
abstain_reason |
A short reason string, or |
baseline |
The |
method |
A label recording the valuation rule. |
inputs |
A named list recording input provenance. |
metadata |
A named list of free-form metadata. |
The abstain_reason vocabulary extends the decision vocabulary with the
reasons specific to pricing: input_ungrounded, no_feasible_action,
low_ess, degenerate_utility (all inherited from the decision that the
valuation is built on) and undefined_ratio (a benefit-cost ratio whose
discounted cost base is not strictly positive).
A valuation S7 object.
decide_evpi(), decide_evsi(), decide_bcr().
Other value:
bcr_crrdc(),
decide_bcr(),
decide_evpi(),
decide_evsi()
v <- valuation(value = 12.4, type = "evpi", grounding = grounding_grounded()) v@valuev <- valuation(value = 12.4, type = "evpi", grounding = grounding_grounded()) v@value