Skip to content

Port the allocation and scoring core - #9

Merged
nickreich merged 1 commit into
nr/scaffolding/1from
nr/core-port/2
Sep 2, 2026
Merged

nickreich merged 1 commit into
nr/scaffolding/1from
nr/core-port/2

Conversation

@nickreich

Copy link
Copy Markdown
Member

Supersedes #7, which GitHub locked closed after a force-push to an unrelated history. Same branch, same content.

The hub-agnostic core: the budget-constrained optimizer and the score built on
it.

Stacked on the scaffolding PR — this PR targets nr/scaffolding/1, so the diff shows only
this layer. GitHub will retarget it to main automatically once #1 merges.

What it does

allocate() solves

min_x  sum_i E_{Y_i ~ F_i} L_i(x_i, Y_i)   s.t.  sum_i w_i x_i <= K,  x_i >= 0

where each L_i is a generalized piecewise linear (pinball) loss. The problem
is separable and convex, so at the optimum every target receiving a positive
allocation has the same marginal expected benefit. allocate() bisects on that
common value — the shadow price of the budget — which makes each allocation a
predictive quantile discounted by that price. All values of K are solved
simultaneously, sharing root-finding work wherever they currently agree on the
multiplier.

alloscore() then charges the realized loss of that allocation against the loss
of an oracle that knew the outcomes, so a perfect forecast scores zero.

Verification

The original package had exactly one test, the usethis placeholder
expect_equal(2 * 2, 4). This PR adds 306.

  • Unit tests for every exported function, including closed-form checks of
    the expected losses against the analytic normal formulae, finite-difference
    checks of their derivatives, and a check that the unconstrained optimum is the
    alpha-quantile.
  • test-zxh.R reproduces published optima. The zxh_tab2 and zxh_tab3
    datasets carry the optimal solutions to two budget-constrained multiproduct
    newsboy problems from Zhang, Xu and Hua (2009) in their Opt column, so this
    is ground truth external to both versions of the package. allocate()
    reproduces Table 2 to within 0.06 of a unit and picks out exactly the 6 of 17
    products that ZXH stock. data-raw/zxh.R transcribes both tables and
    reproduces the original .rda files exactly.

Before writing any of it I compared each ported function against the original
directly: every loss function, pdqr builder and allocate() path is
bit-identical (max|diff| = 0), including the negative-kappa case the
optimizer relies on, the distfromq path, and the g = log(x) / alpha = 1
special case. PR 4 freezes that into CI fixtures.

Bug fixes

Listed in NEWS.md. The substantive ones: the O/U cost parameterization
errored or silently returned NULL; dg and exp_gpl_loss_fun()'s offset
were accepted and ignored; new_gpl_df() referred to an undefined variable;
alloscore.slim() ignored against_oracle; allocate() returned an unusable
object when no target had any marginal benefit; and every dplyr and tidyr call
is now namespaced and declared, where the original only worked with the
tidyverse attached.

Known limitations, filed rather than fixed

Three pre-existing behaviours are pinned by tests rather than changed, so that
altering them has to be deliberate: #3 (negative allocations when the
alpha-quantile is negative), #4 (the oracle spends the whole budget even when
it has no use for it) and #5 (g = "log(x)" cannot be bracketed). #1 and #2 are
also visible from this layer.

Verified locally

R CMD check: Status: OK. 306 tests, 0 failures, 0 warnings, 0 skips.
lintr: 0 lints. air format . --check: clean.

The hub-agnostic core: the budget-constrained optimizer and the score built
on it.

`allocate()` solves `min_x sum_i E L_i(x_i, Y_i)` subject to `sum_i w_i x_i <= K`
by bisecting on one Lagrange multiplier, the shadow price of the budget, so each
allocation is a predictive quantile discounted by that price. `alloscore()` then
charges the realized loss of that allocation against the loss of an oracle that
knew the outcomes.

Verification, where the original package had a single
`expect_equal(2 * 2, 4)` placeholder:

* Unit tests for every exported function, including closed-form checks of the
  expected losses against the analytic normal formulae and finite-difference
  checks of their derivatives.
* `test-zxh.R` reproduces the published optima in Zhang, Xu and Hua (2009),
  which the `zxh_tab2` and `zxh_tab3` datasets carry in their `Opt` column.
  That is ground truth external to both versions of this package. `data-raw/zxh.R`
  transcribes those tables and reproduces the original `.rda` files exactly.

Bug fixes are listed in `NEWS.md`. Before writing the tests I compared each
ported function against the original directly: every loss function, `pdqr`
builder and `allocate()` path is bit-identical, including the negative-`kappa`
case the optimizer relies on and the `g = log(x)` / `alpha = 1` special case.
PR 4 pins that in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nickreich
nickreich merged commit 4fededb into nr/scaffolding/1 Sep 2, 2026
1 check passed
@nickreich
nickreich deleted the nr/core-port/2 branch September 2, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant