fitPS can fit models defined outside the package. This
is useful when the built-in zeta, ZIZ, or logarithmic models do not
represent the probability distribution required for an analysis. A
separate package or an ordinary R script can define a
psModel subclass, provide the required public S3 methods,
and pass that model directly to fit().
The division of responsibilities is straightforward: the new model
supplies its statistical mathematics, while fitPS supplies
the common fitting and fitted-object interfaces. The model therefore
needs to describe its parameters, observation mapping, probabilities,
likelihood, priors when Bayesian fitting is required, parameter
constraints or transformations, and suitable starting information.
This vignette develops two examples:
mu and sigma, in which a latent normal
log-rate is integrated out of the Poisson likelihood.Neither example requires editing or rebuilding
fitPS.
An external model starts with the following public maximum-likelihood pieces:
psModel() to construct the model descriptor;modelObservationData() to map a psData
object to the values used by the distribution;modelProbabilities() to calculate fitted or predicted
probability terms;modelLogLikelihood() to calculate the model log
likelihood; andmodelMleControl() when the defaults stored
in the model descriptor are not sufficient.The descriptor also records parameter names.
supportedPosteriorEngines() reports Bayesian engines
explicitly supported by the model. An external model does not need to
advertise a Bayesian engine simply to participate in maximum-likelihood
fitting.
To add Bayesian fitting, the model normally supplies
modelLogPrior() and modelBayesControl().
Models that can be fitted by the generic MCMC engine also supply
modelToUnconstrained(),
modelFromUnconstrained(), and
modelLogJacobian() when any natural parameter is
constrained. The model supplies this transformation mathematics; it does
not implement the numerical integrator or MCMC sampler.
A model should be expressed to users on its natural parameter scale. For example, a Poisson rate satisfies
\[ \lambda > 0, \]
a probability satisfies
\[ 0 < p < 1, \]
and the zeta shape used by fitPS satisfies
\[ \alpha > 1. \]
Those constraints are scientifically meaningful, but they are awkward
for a generic random-walk MCMC algorithm. A proposal made directly on
the natural scale can easily produce an impossible value such as a
negative rate or a probability greater than one. The generic
fitPS MCMC engine therefore proposes on an
unconstrained scale, where every coordinate can range
over the whole real line.
For MCMC, the model describes its transformation with three methods:
modelToUnconstrained(model, parameters) maps a named
vector of natural parameters \(\theta\)
to unconstrained coordinates \(z\).modelFromUnconstrained(model, unconstrained) maps \(z\) back to the natural parameters \(\theta\). It must be the inverse of
modelToUnconstrained() over the parameter region used for
Bayesian fitting.modelLogJacobian(model, unconstrained) returns the
change-of-variables correction\[ \log\left|\det\left(\frac{\partial \theta}{\partial z}\right)\right|. \]
The direction of that Jacobian is important: it is the derivative of the transformation from unconstrained coordinates back to natural parameters. The MCMC target on the unconstrained scale is therefore
\[ \log p(z\mid x) = \log L\{\theta(z);x\} + \log \pi\{\theta(z)\} + \log\left|\det\left(\frac{\partial \theta}{\partial z}\right)\right|. \]
For a positive parameter, a common choice is
\[ z = \log(\theta), \qquad \theta = \exp(z), \]
so modelLogJacobian() returns \(z\). For a probability,
\[ z = \operatorname{logit}(p), \qquad p = \operatorname{logit}^{-1}(z), \]
and the log-Jacobian is
\[ \log(p) + \log(1-p). \]
If a natural parameter is already unconstrained, the identity transformation is appropriate and its log-Jacobian contribution is zero. For several independently transformed parameters, the log-Jacobian is the sum of the component terms. A coupled multivariate transformation instead requires the log absolute determinant of the full inverse-transformation Jacobian matrix.
All three transformation methods must return or accept named vectors
whose names and order match modelParameterNames(model).
Keeping the names and order exact is important because the MCMC fitter
cannot infer a model-specific parameter ordering.
The deterministic numerical engines are different: they integrate
over the model’s natural parameter scale using the bounds supplied by
modelBayesControl(). They do not use these MCMC
transformations. One-parameter models use one-dimensional numerical
integration, two-parameter models use adaptive cubature, and models with
three or more parameters should use MCMC. If a higher-dimensional model
explicitly requests the numerical engine, fitPS refuses
rather than silently changing the requested engine.
Once these pieces are supplied, fit() returns an
ordinary psFit. Generic operations such as
fitted(), predict(), logLik(),
deviance(), AIC(), BIC(),
posterior summaries, and serialization continue to be handled by
fitPS where applicable.
A model method receives fitPS probability labels. P surveys are
labelled P0, P1, … and S surveys are labelled
S1, S2, …. The model must place its
probability sequence on those labels without changing the shape of the
distribution.
For a distribution with natural support 0, 1, 2, ...,
the same sequence
\[ p_0, p_1, p_2, \ldots \]
is attached to P0, P1, P2, ... for a P survey and to
S1, S2, S3, ... for an S survey. Thus an S label
s is evaluated at natural support value s - 1.
This is a support shift only: there is no truncation and no
renormalisation.
The following helper is used by both zero-based examples in this vignette:
zeroBasedSurveySupport = function(n, type) {
type = match.arg(type, c("P", "S"))
if (identical(type, "P")) {
return(n)
}
n - 1L
}A distribution whose natural support starts at 1 can use a different
mapping. For example, the logarithmic model’s first probability is
naturally associated with both P0 and S1. Each
model therefore defines this support mapping explicitly rather than
relying on a single hard-coded truncation rule.
For a Poisson random variable with rate lambda,
\[ P(Y = y \mid \lambda) = \frac{e^{-\lambda}\lambda^y}{y!}, \qquad y = 0, 1, 2, \ldots \]
For a P survey, probability label Pp uses the natural
support value p. For an S survey, label Ss
uses s - 1. Thus
\[ P(P_p \mid \lambda) = \frac{e^{-\lambda}\lambda^p}{p!}, \qquad p = 0, 1, 2, \ldots, \]
and
\[ P(S_s \mid \lambda) = \frac{e^{-\lambda}\lambda^{s-1}}{(s-1)!}, \qquad s = 1, 2, 3, \ldots. \]
These are the same probability sequence with different survey labels.
If the observed count value y_i occurs with survey
frequency r_i, the weighted log likelihood used by these
examples is
\[ \ell(\lambda) = \sum_i r_i \left[ -\lambda + y_i \log(\lambda) - \log(y_i!) \right]. \]
This closed-form probability mass function makes Poisson a useful minimal example of the extension interface.
The model constructor contains only declarative information. The
Poisson model has one parameter, lambda, and uses a
positive lower bound for generic maximum-likelihood optimisation.
externalPoissonModel = function() {
psModel(
model = "poisson",
parameterNames = "lambda",
subclass = "externalPoissonModel",
supportedEngines = c("numerical", "mcmc"),
mleStart = c(lambda = 1),
mleLower = c(lambda = sqrt(.Machine$double.eps))
)
}The resulting object is a psModel subclass, but it is
not a built-in fitPS distribution.
poissonModel = externalPoissonModel()
class(poissonModel)
#> [1] "externalPoissonModel" "psModel"
modelParameterNames(poissonModel)
#> [1] "lambda"
supportedPosteriorEngines(poissonModel)
#> [1] "numerical" "mcmc"This model advertises both numerical and
mcmc. Because it has one parameter, Bayesian fitting
defaults to the deterministic numerical engine unless MCMC is requested
explicitly.
The Poisson distribution is naturally zero-based. P observations can therefore be used directly, while S labels are shifted down by one before the Poisson mass function is evaluated.
modelProbabilities() receives named parameter values and
the probability terms requested by the generic fitted/prediction
machinery.
modelProbabilities.externalPoissonModel = function(model,
parameters,
n,
type,
...) {
lambda = parameters[["lambda"]]
support = zeroBasedSurveySupport(n, type)
values = vapply(
support,
function(value) {
dpois(value, lambda = lambda)
},
numeric(length(lambda))
)
if (length(lambda) == 1L) {
values = matrix(values, nrow = 1L)
}
colnames(values) = paste0(type, n)
values
}The likelihood is weighted by the survey frequencies stored in the
psData object.
For Bayesian fitting, use a model-specific prior object. Here a Gamma
prior is convenient for the positive Poisson rate.
modelLogPrior() evaluates that prior, while
modelBayesControl() supplies a sensible natural-scale start
and bounds for deterministic integration.
modelLogPrior.externalPoissonModel = function(model, parameters, prior, ...) {
lambda = parameters[["lambda"]]
if (!is.list(prior) ||
!all(c("shape", "rate") %in% names(prior)) ||
any(!is.finite(c(prior$shape, prior$rate))) ||
prior$shape <= 0 || prior$rate <= 0) {
stop("Poisson prior must contain positive finite shape and rate values")
}
dgamma(lambda, shape = prior$shape, rate = prior$rate, log = TRUE)
}
modelBayesControl.externalPoissonModel = function(model, x, engine, prior, ...) {
observations = modelObservationData(model, x)
start = weighted.mean(observations, x$data$rn)
if (!is.finite(start) || start <= 0) {
start = 1
}
list(
start = c(lambda = start),
lower = c(lambda = 0),
upper = c(lambda = Inf)
)
}
modelToUnconstrained.externalPoissonModel = function(model, parameters, ...) {
c(lambda = log(parameters[["lambda"]]))
}
modelFromUnconstrained.externalPoissonModel = function(model, unconstrained, ...) {
c(lambda = exp(unconstrained[["lambda"]]))
}
modelLogJacobian.externalPoissonModel = function(model, unconstrained, ...) {
unname(unconstrained[["lambda"]])
}Here lambda is positive, so MCMC uses
log(lambda) as its unconstrained coordinate.
modelFromUnconstrained() exponentiates that coordinate to
recover the natural Poisson rate, and modelLogJacobian()
returns the unconstrained value because
d exp(z) / dz = exp(z). The one-dimensional numerical
engine instead integrates lambda directly on its natural
scale and therefore does not use these transformation methods.
A downstream package should register these methods in its
NAMESPACE, for example:
S3method(modelObservationData,externalPoissonModel)
S3method(modelProbabilities,externalPoissonModel)
S3method(modelLogLikelihood,externalPoissonModel)
S3method(modelLogPrior,externalPoissonModel)
S3method(modelBayesControl,externalPoissonModel)
S3method(modelToUnconstrained,externalPoissonModel)
S3method(modelFromUnconstrained,externalPoissonModel)
S3method(modelLogJacobian,externalPoissonModel)
When experimenting interactively, the equivalent registration can be
performed with base R’s registerS3method(). For a
self-contained script or vignette, a small helper avoids repeating the
same registration boilerplate:
registerModelMethods = function(className, methods) {
fitpsNamespace = asNamespace("fitPS")
for (methodName in methods) {
methodFunctionName = paste0(methodName, ".", className)
methodFunction = get(
methodFunctionName,
envir = parent.frame(),
inherits = TRUE
)
registerS3method(
methodName,
className,
methodFunction,
envir = fitpsNamespace
)
}
}The Poisson methods can then be registered in one call:
registerModelMethods(
"externalPoissonModel",
c(
"modelObservationData",
"modelProbabilities",
"modelLogLikelihood",
"modelLogPrior",
"modelBayesControl",
"modelToUnconstrained",
"modelFromUnconstrained",
"modelLogJacobian"
)
)No access to unexported fitPS functions is required.
Create a small P-survey data set and pass the external model directly
to fit().
poissonData = makePSData(
n = c(0, 1, 2, 3),
count = c(30, 12, 5, 1),
type = "P"
)
poissonFit = fit(
poissonData,
model = externalPoissonModel(),
nterms = 5
)
poissonFit$lambda
#> [1] 0.520834The result is an ordinary psFit, and it retains the
originating external model object.
class(poissonFit)
#> [1] "psFit"
class(poissonFit$modelObject)
#> [1] "externalPoissonModel" "psModel"
fitted(poissonFit)
#> P0 P1 P2 P3 P4
#> 0.594024915 0.309388382 0.080569997 0.013987865 0.001821339
predict(poissonFit, newdata = 0:4, interval = "none")
#> P0 P1 P2 P3 P4
#> 0.594024915 0.309388382 0.080569997 0.013987865 0.001821339The standard model-comparison functions also work without any Poisson-specific AIC or BIC code.
logLik(poissonFit)
#> 'log Lik.' -46.56563 (df=1)
deviance(poissonFit)
#> [1] 93.13125
AIC(poissonFit)
#> [1] 95.13125
BIC(poissonFit)
#> [1] 97.00245The same external model can now be fitted Bayesianly without
implementing an integration routine or sampler. With one parameter and
numerical support, fitPS chooses numerical integration by
default.
poissonPrior = list(shape = 2, rate = 1)
poissonBayesFit = fit(
poissonData,
model = externalPoissonModel(),
method = "bayes",
prior = poissonPrior,
nterms = 5
)
summary(poissonBayesFit)
#> Summary of fitPS posterior approximation
#> Method: numerical
#>
#> Parameter summaries:
#> parameter estimate sd
#> lambda 0.5510204 0.1060439
#>
#> Posterior probability summaries:
#> term estimate sd lower upper level
#> P0 0.579567528 0.060399483 0.4596219164 0.695491153 0.95
#> P1 0.312966464 0.027271636 0.2525585627 0.357287579 0.95
#> P2 0.087630609 0.023950917 0.0458566779 0.138868937 0.95
#> P3 0.016941918 0.007918201 0.0055507520 0.035983305 0.95
#> P4 0.002541288 0.001723587 0.0005039208 0.006992915 0.95
#> posteriorMethod
#> numerical
#> numerical
#> numerical
#> numerical
#> numerical
#>
#> Diagnostics:
#> $model
#> [1] "poisson"
#>
#> $dimension
#> [1] 1
#>
#> $integrationMethod
#> [1] "integrate"
#>
#> $bounds
#> lower upper
#> 0 Inf
#>
#> $mode
#> lambda
#> 0.5306124
#>
#> $normalisingError
#> [1] 4.511815e-05
#>
#> $meanError
#> [1] 1.169435e-05
#>
#> $secondMomentError
#> [1] 5.583551e-06
#>
#> $summaryGridSize
#> [1] 513
#>
#> $summaryIntegrationRule
#> [1] "simpson"
#>
#> $generic
#> [1] TRUE
posteriorProbs(poissonBayesFit, n = 5)
#> term estimate sd lower upper level
#> 1 P0 0.579567528 0.060399483 0.4596219164 0.695491153 0.95
#> 2 P1 0.312966464 0.027271636 0.2525585627 0.357287579 0.95
#> 3 P2 0.087630609 0.023950917 0.0458566779 0.138868937 0.95
#> 4 P3 0.016941918 0.007918201 0.0055507520 0.035983305 0.95
#> 5 P4 0.002541288 0.001723587 0.0005039208 0.006992915 0.95
#> posteriorMethod
#> 1 numerical
#> 2 numerical
#> 3 numerical
#> 4 numerical
#> 5 numericalMCMC can be requested explicitly with
bayesOptions = list(posteriorMethod = "mcmc"); the external
model still supplies only its mathematics, while fitPS owns
the sampler.
The same probability shape can be placed on S-survey labels simply by shifting the support. The following S data contain the same frequency sequence one label to the right, so they produce the same fitted Poisson model.
poissonSData = makePSData(
n = c(1, 2, 3, 4),
count = c(30, 12, 5, 1),
type = "S"
)
poissonSFit = fit(
poissonSData,
model = externalPoissonModel(),
nterms = 5
)
c(P = poissonFit$lambda, S = poissonSFit$lambda)
#> P S
#> 0.520834 0.520834
unname(fitted(poissonFit))
#> [1] 0.594024915 0.309388382 0.080569997 0.013987865 0.001821339
unname(fitted(poissonSFit))
#> [1] 0.594024915 0.309388382 0.080569997 0.013987865 0.001821339mu and
sigmaThe second example is deliberately more demanding because its marginal likelihood contains an integral.
Let the latent log-rate be normal,
\[ Z \sim N(\mu, \sigma^2), \]
and let the observed count be conditionally Poisson,
\[ Y \mid Z = z \sim \operatorname{Poisson}(e^z). \]
The parameters mu and sigma therefore have
the same interpretation and naming convention as the corresponding
arguments to dnorm(): they describe the mean and standard
deviation of the latent normal variable Z.
After integrating out Z, the marginal probability of a
count y is
\[ P(Y = y \mid \mu, \sigma) = \int_{-\infty}^{\infty} \frac{\exp(-e^z)e^{yz}}{y!} \frac{1}{\sigma\sqrt{2\pi}} \exp\left\{-\frac{(z-\mu)^2}{2\sigma^2}\right\} \, dz. \]
Equivalently,
\[ P(Y = y \mid \mu, \sigma) = \int_{-\infty}^{\infty} P(Y=y\mid Z=z) f_Z(z;\mu,\sigma)\,dz. \]
The fitPS survey labels again change only the support location. If
\[ g(k; \mu, \sigma) = \int_{-\infty}^{\infty} P(Y=k\mid Z=z) f_Z(z;\mu,\sigma)\,dz, \]
then
\[ P(P_p \mid \mu, \sigma) = g(p; \mu, \sigma) \]
and
\[ P(S_s \mid \mu, \sigma) = g(s-1; \mu, \sigma). \]
There is no simple closed-form expression for this integral, so this
example evaluates it numerically. If y_i occurs with
frequency r_i, the marginal log likelihood is
\[ \ell(\mu,\sigma) = \sum_i r_i \log\left\{ \int_{-\infty}^{\infty} P(Y=y_i\mid Z=z) f_Z(z;\mu,\sigma)\,dz \right\}, \qquad \sigma > 0. \]
This second example is deliberately more demanding than the ordinary
Poisson model. Each likelihood contribution contains an inner numerical
integral, so it demonstrates how a model can retain its own specialised
calculations while still using the standard fitPS fitting
interface.
The Poisson-normal count distribution is also naturally zero-based, so it uses the same shape-preserving P/S support mapping as the ordinary Poisson example. Starting values can be obtained from the marginal mean and variance of the Poisson-normal model,
\[ E(Y) = \exp\left(\mu + \frac{\sigma^2}{2}\right), \]
and
\[ \operatorname{Var}(Y) = E(Y) + E(Y)^2\left\{\exp(\sigma^2)-1\right\}. \]
These moment relationships provide convenient initial values for
mu and sigma without changing the model
parameterization.
modelObservationData.externalPoissonNormalModel = function(model, x, ...) {
zeroBasedSurveySupport(x$data$n, x$type)
}
modelMleControl.externalPoissonNormalModel = function(model, x, ...) {
observations = modelObservationData(model, x)
weights = x$data$rn
meanStart = weighted.mean(observations, weights)
varianceStart = weighted.mean((observations - meanStart)^2, weights)
if (!is.finite(meanStart) || meanStart <= 0) {
meanStart = 1
}
if (!is.finite(varianceStart)) {
varianceStart = meanStart
}
extraVariance = max(
varianceStart - meanStart,
meanStart^2 * 1e-6
)
sigmaSquaredStart = log1p(extraVariance / meanStart^2)
sigmaStart = sqrt(max(sigmaSquaredStart, 1e-6))
muStart = log(meanStart) - sigmaSquaredStart / 2
list(
start = c(mu = muStart, sigma = sigmaStart),
lower = c(mu = -20, sigma = sqrt(.Machine$double.eps)),
upper = c(mu = 20, sigma = 5)
)
}The distribution-specific integral that defines the Poisson-normal
probability remains downstream code. For numerical stability, integrate
over a standard Normal variable u with
z = mu + sigma * u; this avoids asking
integrate() to resolve an increasingly narrow Normal
density when sigma is small. The outer Bayesian posterior
integration is still owned by fitPS.
poissonNormalProbability = function(n, mu, sigma) {
if (!is.finite(mu) || !is.finite(sigma) || sigma <= 0) {
return(rep(NaN, length(n)))
}
vapply(n, function(value) {
if (!is.finite(value) || value < 0 || value != floor(value)) {
return(0)
}
integrand = function(u) {
z = mu + sigma * u
dpois(value, lambda = exp(z)) * dnorm(u)
}
result = integrate(
integrand,
lower = -Inf,
upper = Inf,
rel.tol = 1e-8,
subdivisions = 200L,
stop.on.error = FALSE
)
if (!identical(result$message, "OK")) {
return(NaN)
}
result$value
}, numeric(1L))
}The fitPS-facing methods simply expose those marginal probabilities and their weighted log likelihood.
modelProbabilities.externalPoissonNormalModel = function(model,
parameters,
n,
type,
...) {
parameterFrame = as.data.frame(parameters)
if (!all(c("mu", "sigma") %in% names(parameterFrame))) {
stop("parameters must contain mu and sigma")
}
support = zeroBasedSurveySupport(n, type)
values = vapply(seq_len(nrow(parameterFrame)), function(row) {
poissonNormalProbability(
support,
mu = parameterFrame$mu[row],
sigma = parameterFrame$sigma[row]
)
}, numeric(length(support)))
values = t(values)
colnames(values) = paste0(type, n)
values
}
modelLogLikelihood.externalPoissonNormalModel = function(model,
parameters,
data,
...) {
mu = parameters[["mu"]]
sigma = parameters[["sigma"]]
observations = modelObservationData(model, data)
probabilities = poissonNormalProbability(
observations,
mu = mu,
sigma = sigma
)
if (any(!is.finite(probabilities)) || any(probabilities <= 0)) {
return(-Inf)
}
sum(data$data$rn * log(probabilities))
}The two-parameter example uses a Normal prior for mu and
a half-Normal prior for positive sigma. The Bayesian
control reuses the finite MLE bounds; these give the two-dimensional
numerical engine a finite integration rectangle. MCMC leaves the
unconstrained location parameter mu unchanged and uses a
log transform for positive sigma. Consequently the
two-dimensional inverse transformation is
(mu, exp(zSigma)), whose Jacobian determinant is
exp(zSigma) and whose log-Jacobian is simply
zSigma.
modelLogPrior.externalPoissonNormalModel = function(model, parameters, prior, ...) {
mu = parameters[["mu"]]
sigma = parameters[["sigma"]]
required = c("muMean", "muSd", "sigmaScale")
if (!is.list(prior) || !all(required %in% names(prior)) ||
any(!is.finite(unlist(prior[required], use.names = FALSE))) ||
prior$muSd <= 0 || prior$sigmaScale <= 0) {
stop("Poisson-normal prior must contain muMean, positive muSd, and positive sigmaScale")
}
if (!is.finite(sigma) || sigma <= 0) {
return(-Inf)
}
dnorm(mu, mean = prior$muMean, sd = prior$muSd, log = TRUE) +
log(2) + dnorm(sigma, mean = 0, sd = prior$sigmaScale, log = TRUE)
}
modelBayesControl.externalPoissonNormalModel = function(model, x, engine, prior, ...) {
modelMleControl(model, x)
}
modelToUnconstrained.externalPoissonNormalModel = function(model, parameters, ...) {
c(mu = parameters[["mu"]], sigma = log(parameters[["sigma"]]))
}
modelFromUnconstrained.externalPoissonNormalModel = function(model, unconstrained, ...) {
c(mu = unconstrained[["mu"]], sigma = exp(unconstrained[["sigma"]]))
}
modelLogJacobian.externalPoissonNormalModel = function(model, unconstrained, ...) {
unname(unconstrained[["sigma"]])
}A downstream package would normally use these namespace registrations:
S3method(modelObservationData,externalPoissonNormalModel)
S3method(modelMleControl,externalPoissonNormalModel)
S3method(modelProbabilities,externalPoissonNormalModel)
S3method(modelLogLikelihood,externalPoissonNormalModel)
S3method(modelLogPrior,externalPoissonNormalModel)
S3method(modelBayesControl,externalPoissonNormalModel)
S3method(modelToUnconstrained,externalPoissonNormalModel)
S3method(modelFromUnconstrained,externalPoissonNormalModel)
S3method(modelLogJacobian,externalPoissonNormalModel)
For this self-contained vignette, the same
registerModelMethods() helper used above keeps the
registration concise:
The following frequencies were generated from a Poisson-normal
distribution with values close to mu = 0.2 and
sigma = 0.45.
poissonNormalData = makePSData(
n = 0:8,
count = c(3032, 3240, 2035, 997, 426, 168, 64, 24, 9),
type = "P"
)
poissonNormalFit = fit(
poissonNormalData,
model = externalPoissonNormalModel(),
nterms = 9
)
c(mu = poissonNormalFit$mu, sigma = poissonNormalFit$sigma)
#> mu sigma
#> 0.2022064 0.4385527The same common fitted-object and model-comparison APIs remain available even though every probability evaluation requires downstream numerical integration.
fitted(poissonNormalFit)
#> P0 P1 P2 P3 P4
#> 0.3020764943 0.3253416965 0.2046319036 0.0997539960 0.0421922633
#> P5 P6 P7 P8
#> 0.0164322567 0.0061063510 0.0022152224 0.0007966113
predict(poissonNormalFit, newdata = 0:8, interval = "none")
#> P0 P1 P2 P3 P4
#> 0.3020764943 0.3253416965 0.2046319036 0.0997539960 0.0421922633
#> P5 P6 P7 P8
#> 0.0164322567 0.0061063510 0.0022152224 0.0007966113
logLik(poissonNormalFit)
#> 'log Lik.' -15370.36 (df=2)
deviance(poissonNormalFit)
#> [1] 30740.72
AIC(poissonNormalFit)
#> [1] 30744.72
BIC(poissonNormalFit)
#> [1] 30759.14As with Poisson, S-survey labels shift the same probability sequence one place to the right rather than truncating it. The fitted parameter values therefore describe the same underlying distribution; only the survey labels attached to its probabilities change.
poissonNormalP = modelProbabilities(
externalPoissonNormalModel(),
parameters = list(mu = poissonNormalFit$mu, sigma = poissonNormalFit$sigma),
n = 0:8,
type = "P"
)
poissonNormalS = modelProbabilities(
externalPoissonNormalModel(),
parameters = list(mu = poissonNormalFit$mu, sigma = poissonNormalFit$sigma),
n = 1:9,
type = "S"
)
all.equal(
as.numeric(poissonNormalP),
as.numeric(poissonNormalS)
)
#> [1] TRUEBecause the model has two parameters and advertises numerical support, Bayesian fitting defaults to adaptive two-dimensional cubature. The following smaller data set keeps vignette rendering quick while exercising the complete external Bayesian path.
poissonNormalBayesData = makePSData(
n = 0:2,
count = c(30, 12, 4),
type = "P"
)
poissonNormalPrior = list(muMean = 0, muSd = 2, sigmaScale = 1)
poissonNormalBayesFit = fit(
poissonNormalBayesData,
model = externalPoissonNormalModel(),
method = "bayes",
prior = poissonNormalPrior,
nterms = 4
)
summary(poissonNormalBayesFit)
#> Summary of fitPS posterior approximation
#> Method: numerical
#>
#> Parameter summaries:
#> parameter estimate sd
#> mu -0.9626080 0.2750007
#> sigma 0.4200006 0.3000013
#>
#> Posterior probability summaries:
#> term estimate sd lower upper level
#> P0 0.67704278 0.017534774 0.635921812 0.69220063 0.95
#> P1 0.25104661 0.009208520 0.231547000 0.25470288 0.95
#> P2 0.05677074 0.009341015 0.046839584 0.07850209 0.95
#> P3 0.01124096 0.006310165 0.005743773 0.02928890 0.95
#> posteriorMethod
#> numerical
#> numerical
#> numerical
#> numerical
#>
#> Diagnostics:
#> $model
#> [1] "poissonNormal"
#>
#> $dimension
#> [1] 2
#>
#> $integrationMethod
#> [1] "hcubature"
#>
#> $bounds
#> $bounds$lower
#> mu sigma
#> -2.000000e+01 1.490116e-08
#>
#> $bounds$upper
#> mu sigma
#> 20 5
#>
#>
#> $mode
#> mu sigma
#> -8.226782e-01 8.940697e-08
#>
#> $cubatureError
#> [1] 6.287419e-07 8.932673e-07 8.007373e-07 1.626728e-06 1.016493e-06
#> [6] 1.024423e-06 4.863355e-05
#>
#> $cubatureReturnCode
#> [1] 0
#>
#> $expectedDeviance
#> [1] 80.44896
#>
#> $functionEvaluations
#> [1] 8245
#>
#> $tolerance
#> [1] 1e-05
#>
#> $summaryGridSize
#> [1] 41
#>
#> $summaryIntegrationRule
#> [1] "simpson x simpson"
#>
#> $generic
#> [1] TRUE
posteriorProbs(poissonNormalBayesFit, n = 4)
#> term estimate sd lower upper level
#> 1 P0 0.67704278 0.017534774 0.635921812 0.69220063 0.95
#> 2 P1 0.25104661 0.009208520 0.231547000 0.25470288 0.95
#> 3 P2 0.05677074 0.009341015 0.046839584 0.07850209 0.95
#> 4 P3 0.01124096 0.006310165 0.005743773 0.02928890 0.95
#> posteriorMethod
#> 1 numerical
#> 2 numerical
#> 3 numerical
#> 4 numericalThe same model can use the generic MCMC engine by requesting
posteriorMethod = "mcmc". Models with three or more
parameters should advertise and use MCMC rather than the numerical
engine.
New psFit objects retain their originating model
descriptor. That avoids reconstructing third-party models from a
hard-coded list of model names.
class(poissonFit$modelObject)
#> [1] "externalPoissonModel" "psModel"
class(poissonBayesFit$modelObject)
#> [1] "externalPoissonModel" "psModel"
class(poissonNormalFit$modelObject)
#> [1] "externalPoissonNormalModel" "psModel"
class(poissonNormalBayesFit$modelObject)
#> [1] "externalPoissonNormalModel" "psModel"This also means a fitted object can survive ordinary R serialization, provided the corresponding S3 methods are available when the object is used again.
path = tempfile(fileext = ".rds")
saveRDS(poissonNormalBayesFit, path)
restoredFit = readRDS(path)
class(restoredFit$modelObject)
#> [1] "externalPoissonNormalModel" "psModel"
summary(restoredFit)
#> Summary of fitPS posterior approximation
#> Method: numerical
#>
#> Parameter summaries:
#> parameter estimate sd
#> mu -0.9626080 0.2750007
#> sigma 0.4200006 0.3000013
#>
#> Posterior probability summaries:
#> term estimate sd lower upper level
#> P0 0.67704278 0.017534774 0.635921812 0.69220063 0.95
#> P1 0.25104661 0.009208520 0.231547000 0.25470288 0.95
#> P2 0.05677074 0.009341015 0.046839584 0.07850209 0.95
#> P3 0.01124096 0.006310165 0.005743773 0.02928890 0.95
#> posteriorMethod
#> numerical
#> numerical
#> numerical
#> numerical
#>
#> Diagnostics:
#> $model
#> [1] "poissonNormal"
#>
#> $dimension
#> [1] 2
#>
#> $integrationMethod
#> [1] "hcubature"
#>
#> $bounds
#> $bounds$lower
#> mu sigma
#> -2.000000e+01 1.490116e-08
#>
#> $bounds$upper
#> mu sigma
#> 20 5
#>
#>
#> $mode
#> mu sigma
#> -8.226782e-01 8.940697e-08
#>
#> $cubatureError
#> [1] 6.287419e-07 8.932673e-07 8.007373e-07 1.626728e-06 1.016493e-06
#> [6] 1.024423e-06 4.863355e-05
#>
#> $cubatureReturnCode
#> [1] 0
#>
#> $expectedDeviance
#> [1] 80.44896
#>
#> $functionEvaluations
#> [1] 8245
#>
#> $tolerance
#> [1] 1e-05
#>
#> $summaryGridSize
#> [1] 41
#>
#> $summaryIntegrationRule
#> [1] "simpson x simpson"
#>
#> $generic
#> [1] TRUEA downstream package therefore needs to remain installed and loadable when its fitted objects are restored, just as with other S3-based extension systems.
A real external package does not need to copy fitPS fitting code. Its distribution layer can remain small:
psModel(...) with a unique
subclass;S3method(...) entries in its NAMESPACE;
andfit(), fitted
probabilities, prediction, and relevant model-comparison criteria
work.For an MLE-only model, there is no requirement to advertise a posterior engine. For Bayesian models, advertise only the engines supported by the public mathematics supplied by the model. One- and two-parameter models can use generic numerical integration; MCMC is the general route and is required for models with three or more parameters. External model authors do not implement those engines themselves.
The Poisson example shows that a distribution with a closed-form probability mass function can be added without another public fitting function and without any fitPS source modification.
The Poisson-normal example shows that a model can contain substantial
distribution-specific mathematics, including an inner numerical
integral, without requiring a special fitting function. Once the public
model methods are supplied, the model can use the standard
fit(), predict(), logLik(), AIC,
BIC, deviance, and Bayesian interfaces.
The practical rule is: the new model supplies the statistical
mathematics; fitPS supplies the common fitting, posterior,
prediction, and fitted-object interfaces.