skellambrms 0.6.1 (not yet released)
The Skellam log-CCDF was wrong far below the bulk of the distribution, in the Stan functions
skellam1_lccdfandskellam2_lccdfand in their R counterparts. Belownormal_approx_threshold, the CCDF was summed upward fromy + 1and stopped after 500 terms, so forymore than about 495 below the bulk it stopped before reaching the probability mass. Atsigma = 5,skellam1_lccdfreturned -0.48 aty = -499, -134.5 aty = -600and -Inf aty = -1000, where the true value is 0. brms normalises a lower-truncated observation with the log-CCDF atlb - 1, so undertrunc(lb = -y2)the likelihood of any row withy2above about 495 was wrong, as wereposterior_predict()andposterior_epred()for that row. The sum now runs away from the bulk: upward fromy + 1whenyis at or above the mean, and downward fromyotherwise, returning the log of one minus the lower-tail sum. Across 98 cases fromy = -1000to above the mean, the fixed Stan functions agree with the exact CCDF to 1e-8. The previous tests evaluated the log-CCDF no lower thany = -31; new tests coverydown to -1000 and either side of the mean, in R and in Stan. The discrete Laplace and discrete normal families compute the CCDF in closed form and were not affected.posterior_epred_dlaplace2()andposterior_epred_dnorm2()returnedmuon rows without truncation bounds. The discretised variable is distributed as round(X), whose mean equalsmuonly for integer or half-integermu. Fordlaplace2(), the difference reaches 0.015 atsigma = 1, 0.054 atsigma = 0.5and 0.14 atsigma = 0.25; fordnorm2(), it is below 1e-9 atsigma = 1but reaches 0.093 atsigma = 0.25. Both methods now return the exact mean: a closed form for the discrete Laplace, and a series in the characteristic function for the discrete normal. Both agree with direct summation of the PMF to 1e-14 forsigmafrom 0.01 to 100. Rows with truncation bounds were already exact. Two new tests compare each method with summation of the PMF and with the mean ofposterior_predict()draws at non-integermu.The README and
.zenodo.jsonno longer say that all six families share a (mean, SD) convention under which fits are directly comparable. For the Skellam families,muandsigmaare the exact mean and SD of the difference. For the discrete Laplace and discrete normal, they are the location and SD of the continuous distribution before discretisation. The README gives the size of the differences.DESCRIPTION now states a minimum version of brms and the system requirements for fitting.
Imports: brms (>= 2.21.0), the earliest of the brms releases tested (2.15.0, 2.16.0, 2.21.0, 2.23.0) under which the test suite passes. Every post-processing method callsbrms::get_dpar(), which brms has exported since 2.16.0; under 2.16.0, threemake_stancode()tests fail inside brms on R 4.6. A truncatedskellam2()fit was run under brms 2.21.0 with both backends. A newSystemRequirementsfield names the C++17 compiler that rstan or CmdStan uses to compile each model, andLanguage: en-GBdeclares the spelling used in the documentation.The PDF manual failed to build because
?skellam1contained the character “≈”, which LaTeX cannot typeset. The section containing it, on converting priors from the log(mu_skellam) scale of earlier versions, is removed; its statement that slope coefficients do not convert between the two scales was wrong, since a slope on log(mu_skellam) is twice the slope on log(sigma).The help pages of the six families and their
_lccdf_stanvars()functions are rewritten for users of the package. Development history, notes on how results were confirmed, statements about the data the defaultnormal_approx_thresholdwas chosen for, and instructions addressed to maintainers are removed; the bold “notes” in Details become named sections. Three statements are corrected.?dlaplace2said thatskellam2()requiressigma >= |mu|; the constraint issigma^2 >= |mu|, as?skellam2states.?dnorm1and?dnorm1_lccdf_stanvarssaid the Stan code usesnormal_lccdf; it useserfc()for the upper tail, becausenormal_lccdfreturns negative infinity above about 8.25 SDs.?dnorm1also said thatlog_diff_exp()of twonormal_lcdfvalues cancels near 10 SDs; under rstan 2.32 and CmdStan 2.37 that form is accurate to the underflow of the log-PMF near 38 SDs, and the cancellation occurs only in R, withpnorm(). The help pages of the four discrete families now state thatsigmais the SD of the continuous distribution before discretisation, and those ofdlaplace2()anddnorm2()state by how much the mean of the discretised distribution differs frommu.citation("skellambrms")now gives the Zenodo concept DOI, 10.5281/zenodo.22231870, which resolves to the latest archived version. It previously gave the version DOI for 0.6.0, which would have been out of date in every later release.The README no longer calls the joint families in bicountbrms “bivariate negative-binomial”: their margins are not negative binomial; their components are. The hyperlinks to JPEC Consulting and Sea Through Science are removed, because both sites rejected requests from the URL check in
R CMD check. The Funding section still names both organisations.
skellambrms 0.6.0
-
The difference families are back under their original package name. Between 0.5.0 and this release, this code was distributed as
pairedcountbrms(0.6.0–0.8.0), which contained two unrelated suites: these six families, which model the differenced = y1 - y2, and a set that models the count pair jointly. They shared no code — not a helper, not a Stan block, not a dependency — and have been separated again. The joint families are nowbicountbrms. Its 0.9.0 was the matching half of this release, at the time of the split. Install its current version rather than 0.9.0:bicountbrms0.10.0 removed thebipois_cens(),binegbin_cens()andbinegbin_jointnames that 0.9.0 defined, and suppliesbipois(),binegbin()and a_partialobs()constructor for each in their place.No family, dpar or Stan function name changed, so no fitted model needs refitting. brms resolves a fit’s
log_lik_*/posterior_predict_*/posterior_epred_*methods off the attached search path at call time, so a stored fit works as soon as this package is attached. The only source change required islibrary(pairedcountbrms)→library(skellambrms), and anypairedcountbrms::prefix.Note on the version number:
pairedcountbrmsalso had a 0.6.0, a different release of a differently-named package. This release continues this package’s line from 0.5.0. anhsmith/skellambrmsno longer redirects. From the 0.6.0 rename until now, GitHub served a permanent redirect from this path toanhsmith/pairedcountbrms. Recreating the repository here breaks that redirect deliberately: a user who installedskellambrmswanted the Skellam families, and following the redirect would now land them on a package that no longer has any.pak::pak("anhsmith/skellambrms")installs these families again, which is what it did originally.-
Roxygen markdown is enabled, which changes every help page.
DESCRIPTIONgainedRoxygen: list(markdown = TRUE), which this package had never set, so roxygen had been passing markdown through to Rd uninterpreted. Regenerating the Rd files turned that markdown into Rd markup throughout: code spans that had been printing as literal backticks, and bold and italic that had been printing as their own asterisks. The topics also gained cross-references, of which there had been none: each family links to its_lccdf_stanvars()companion and to the families it is meant to be fitted against.Markdown interpretation is retroactive, so every topic was rendered with
tools::Rd2txt()before and after and diffed word by word. One expression had been altered silently. In?skellam1, the prior-translation intercept0.5*log(2) + 0.5*1was read as emphasis between its two asterisks and rendered as0.5log(2) + 0.51, dropping both multiplication signs; it is now a code span. Under?skellam1_lccdf_stanvars, the two guarded failure modes had been running together into one paragraph, and are now a list. Every documented function states what it returns. The six family topics document five functions each and had a single
\value{}describing the constructor alone. Each now gives the return value and shape oflog_lik_*,posterior_predict_*andposterior_epred_*separately, as well as those of the constructor and its_stanvars()companion. Two behaviours that were documented nowhere are stated there:posterior_predict_*draws subject to a row’sresp_trunc()bounds, andposterior_epred_*takes its mean over the truncated distribution on any row that is bounded.Title:andDescription:also quote'brms'and'Stan', which is the convention CRAN applies to software names.Metadata for archiving.
LICENSEandLICENSE.mdnamed “skellambrms authors”, the placeholderusethiswrites, and now name the copyright holder. A.zenodo.jsonrecords the creator, ORCID, affiliation and licence for the Zenodo deposit, and is excluded from the built package.inst/CITATIONcontains a commenteddoifield to fill in once the release is archived, with a note that a manuscript should wait for the DOI rather than cite the repository URL.Wording, across README, NEWS, roxygen and test comments. A sweep against the prose conventions in
CLAUDE.mdreplaced vague verbs of agency and possession with the relations they stood for, split balanced “X, and Y” constructions into separate sentences, and gave bare pronouns and quantifiers their nouns. Two statements were wrong rather than loose. The README said the normal-approximation threshold “guards two confirmed failure modes” when it guards against them, and it describedbipois()as containingskellam2()when the relation is that the difference implied bybipois()isskellam2().The calibration instrument
coverage_recovery()and itsPAIREDCOUNTBRMS_COVERAGEgate were built for the joint families’ dispersion parameters and went with them tobicountbrms. The smoke gate this suite actually calls,recovery_ok(), stays, intests/testthat/helper-recovery.R, which records where the calibration half went and why the gate here should be widened rather than tightened if a correct model starts failing it.make_synthetic_prep()(tests/testthat/helper-prep.R) loses itsvint1/vint2arguments. These families declare a single response and take no supplementary integer data; only the joint families needed them.The package no longer ships a vignette. The one it had fitted a
binegbin()model and went tobicountbrmswith that family. The README documents every family here with runnable examples.
Entries below record this code’s history under its former names. Releases 0.4.0 and 0.5.0 added and extended the joint bivariate-count families and say nothing about the six families in this package; 0.6.0–0.8.0 were made under the name pairedcountbrms, and concern the joint families almost throughout. They are left as written rather than retrospectively edited. The full record of those releases, including everything omitted here, is in bicountbrms’s NEWS.
Of the entries in that span, two concern these families:
-
0.7.0 re-parameterised
skellam1()ontosigma(see that release’s notes for the prior translation) and rewrote the recovery tests as smoke gates. -
0.6.0 renamed the package to
pairedcountbrms.
skellambrms 0.3.2
- Fixed a silent
ifelse()length-collapse bug inlog_lik_dlaplace1():ifelse(test, yes, no)takes its output length fromtest, not from the (vectorised)yes/nobranches. Becausedlaplace1()has no free mean, the CDF-differencing argument built from the observationzis a scalar, whilesigma(and the derivedb) varies across posterior draws — so theifelse()test was evaluated at length 1 and the whole per-observation log-likelihood silently collapsed to length 1 instead ofndraws. This brokebrms::add_criterion(fit, "loo")for everydlaplace1()fit (is.matrix(unnormalized_log_weights) is not TRUE), while sampling,posterior_predict(), and truncation were entirely unaffected — confirmed isolated to this one function’s R-side length handling, not a data or convergence issue. - Applied the same fix pre-emptively to five more internal R-side helpers sharing the identical
ifelse()shape —dlaplace1_lpmf_r(),dlaplace1_lccdf_r(),dlaplace2_lccdf_r(),dnorm1_lpmf_r(), anddnorm2_lpmf_r()inR/truncation.R. None were triggering the bug at their current call sites (which happen to keep argument lengths matched), but all shared the same landmine.
skellambrms 0.3.1
- Fixed
posterior_predict_<family>()for all six families: previously ignoredtrunc()/resp_trunc()bounds entirely, drawing from the untruncated distribution and returning it verbatim (confirmed to produce out-of-bound draws, e.g.posterior_predict_dnorm2()returning values well below alb = -14bound). Now performs correct inverse-CDF sampling within the truncation bounds, reusing each family’s already-validated log-CCDF math (transcribed to R in the new internalR/truncation.R) rather than rejection sampling, which was confirmed empirically slow/low-acceptance for tight bounds — especially costly forskellam2(), whose per-evaluation cost (an iterative Bessel-function tail-sum) is comparatively high. - Fixed
posterior_epred_<family>()for all six families: previously returned the untruncated mean (mu, or0forskellam1()/dlaplace1()/dnorm1()) even when a truncation bound was tight enough to meaningfully shift the conditional expectation, with no warning. Now computes the correct truncated conditional expectation via deterministic numerical summation of the truncated PMF — not Monte Carlo, so the result is exact to a documented tolerance and fully reproducible. -
Known limitation surfaced (not introduced) by this fix:
brms::posterior_epred()— and anything built on it, includingfitted()andconditional_effects()— errors on any truncated fit of a custom family, for all six families here, under the currently installedbrms. This is abrmslimitation: its internal dispatcher checks whether a fit is truncated before checking whether the family is a custom one, and has no fallback to a custom family’s ownposterior_epred_<family>()on the truncated branch. Callposterior_epred_<family>(brms::prepare_predictions(fit))directly as a workaround.brms::posterior_predict()is unaffected and works correctly for truncated fits of every family. See the README’s “Limitations” section for details.
skellambrms 0.3.0
-
Breaking change:
skellam1()now samples onsigma, the SD of the difference (log-linked), rather than the underlying Skellam rate directly.mu_skellam = sigma^2 / 2is derived internally; the Bessel-sum likelihood itself is unchanged. A prior previously written onlog(mu_skellam)translates aslog(sigma) = 0.5*log(2) + 0.5*log(mu_skellam)— e.g. an oldnormal(1, 1.5)becomesnormal(0.847, 0.75). This reparameterisation establishes a common (mean, SD-scale) convention shared by every family below. - Added
skellam2()/skellam2_stanvars()/skellam2_lccdf_stanvars(): the asymmetric Skellam (Koopman parameterisation), with a free mean (mu) and a freesigmaexcess(so thatsigma^2 = |mu| + sigmaexcess^2, guaranteeing Skellam validity for everymuandsigmaexcess >= 0— a corrected constraint relative to the originally-specifiedsigma = sqrt(mu^2 + sigmaexcess^2), which only guarantees the weakersigma >= |mu|and admits invalid (negative-rate) parameter combinations for|mu| < 1). Reduces exactly toskellam1()atmu = 0. - Added
dlaplace1()/dlaplace1_stanvars()/dlaplace1_lccdf_stanvars(): a discrete Laplace distribution (location fixed at 0, freesigma), discretised from the continuous Laplace via CDF differencing. - Added
dlaplace2()/dlaplace2_stanvars()/dlaplace2_lccdf_stanvars(): the free-location/free-scale discrete Laplace, with no constraint couplingmuandsigma— a deliberate structural contrast withskellam2(), for comparing models where bias and spread are structurally coupled against ones where they vary independently. - Added
dnorm1()/dnorm1_stanvars()/dnorm1_lccdf_stanvars(): a discrete normal distribution (location fixed at 0, freesigma), via the same CDF-differencing pattern asdlaplace1(). - Added
dnorm2()/dnorm2_stanvars()/dnorm2_lccdf_stanvars(): the free-location/free-scale discrete normal, structurally analogous todlaplace2(). - Fixed a numerical-stability issue affecting
skellam1_lccdf_stanvars()andskellam2_lccdf_stanvars()’s normal-approximation branch, anddnorm1/dnorm2’s_lpmf/_lccdf: Stan’s built-innormal_lccdfis not safe to call directly in this context. This is a documented Stan limitation, not a guess — the Stan Functions Reference statesnormal_lccdfunderflows to-inffor(y-mu)/sigma > ~8.25, and stan-dev/math#1985 confirmsnormal_lccdf(unlikenormal_lcdf) was never updated with the more accurate Mills-ratio approximation. Fixed via an exacterfc()-based closed form throughout, confirmed to match a trusted R reference to machine precision out to 30+ SDs. - Fixed a Stan-compiler portability bug:
skellam2_lpmf/skellam2_lccdfusedfabs(), which compiles underrstan’s bundled Stan version but is not a valid identifier undercmdstanr’s (useabs(), which is type-generic and already used elsewhere in the same functions). - Added
cmdstanrtoSuggests(previously onlyrstanwas declared, soR CMD check’s isolated test environment could not see an already-installedcmdstanr).
skellambrms 0.2.0
- Added
skellam1_lccdf_stanvars(), providing the log-CCDF of the symmetric Skellam(mu, mu) distribution so that brms’sresp_trunc()can be used withskellam1(), including row-varying truncation bounds. Still the symmetric Skellam(mu, mu) case only — this adds truncation support to the existing family, not a new family or the asymmetric case. - The exact log-CCDF (an iterative Bessel-sum tail) switches to a normal approximation above a configurable
normal_approx_threshold(default100), guarding against a confirmedstd::bad_alloccrash and a confirmed multi-GB memory blowup when an unadapted HMC proposal pushesmuto an extreme value during warmup. See?skellam1_lccdf_stanvarsfor guidance on choosing this threshold for your own data.
skellambrms 0.1.0
- Initial release:
skellam1()andskellam1_stanvars(), a brms custom family for the symmetric Skellam(mu, mu) distribution.