Skip to content

Port the allocation and scoring core - #7

Closed
nickreich wants to merge 4 commits into
nr/scaffolding/1from
nr/core-port/2
Closed

nickreich wants to merge 4 commits into
nr/scaffolding/1from
nr/core-port/2

Conversation

@nickreich

Copy link
Copy Markdown
Member

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

Stacked on #1 — 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.

nickreich and others added 4 commits September 2, 2026 14:46
Sets up an empty but checkable package so that the CI pipeline itself is
validated before any code depends on it.

Contents: `DESCRIPTION`, `NAMESPACE`, licence, `NEWS.md`, the five hubverse
workflows with SHA-pinned actions, `.lintr`, `air.toml`, `codecov.yml`,
`_pkgdown.yml` and the `.github` community files, all adapted from `hubEvals`.

What this proves: the check matrix runs on all five platforms, `lintr` and
`air format --check` gate as intended, and the pkgdown Netlify preview builds.
Heavier dependency resolution (`arrow`, `hubUtils`, `hubExamples` from
r-universe) first gets exercised in the following PRs, as each adds only the
`Imports` it actually uses.

Licence is GPL-3 rather than the hubverse-standard MIT because this package is a
derivative work of `aaronger/alloscore`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reichlab repositories publish pkgdown sites to GitHub Pages rather than
Netlify, so replace the Netlify PR-preview workflow inherited from hubEvals
with the standard r-lib pkgdown workflow, matching reichlab/distfromq.

The Netlify workflow could not pass here: the deploy step is configured with
fails-without-credentials: true and this repository has no NETLIFY_AUTH_TOKEN
or NETLIFY_SITE_ID. The site itself built fine, so the failure was purely a
missing third-party credential.

pkgdown still builds on every pull request, so a broken site is still caught in
review; it now deploys to the gh-pages branch on push to main and on release.
Actions remain SHA-pinned per hubverse security policy, which distfromq's copy
does not do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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

Copy link
Copy Markdown
Member Author

Same cause as #6. Replaced by #9.

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