use super::*;
pub struct DispersionLocationScalePredictor {
pub beta_mu: Array1<f64>,
pub beta_noise: Array1<f64>,
pub likelihood: LikelihoodSpec,
pub inverse_link: Option<InverseLink>,
pub covariance: Option<Array2<f64>>,
}
impl DispersionLocationScalePredictor {
fn strategy(&self) -> ResolvedFamilyStrategy {
strategy_for_family(self.likelihood.clone(), self.inverse_link.as_ref())
}
fn eta_mean(&self, input: &PredictInput) -> Array1<f64> {
input.design.dot(&self.beta_mu) + &input.offset
}
fn precision(&self, input: &PredictInput) -> Result<Array1<f64>, EstimationError> {
let design_noise = input.design_noise.as_ref().ok_or_else(|| {
EstimationError::InvalidInput(
"dispersion location-scale prediction requires noise design matrix".to_string(),
)
})?;
let mut eta_d = design_noise.dot(&self.beta_noise);
if let Some(offset_noise) = input.offset_noise.as_ref() {
if offset_noise.len() != eta_d.len() {
return Err(EstimationError::InvalidInput(format!(
"dispersion location-scale noise offset length mismatch: expected {}, got {}",
eta_d.len(),
offset_noise.len()
)));
}
eta_d += offset_noise;
}
Ok(eta_d.mapv(|v| v.exp().max(f64::MIN_POSITIVE)))
}
fn noise_sd(&self, input: &PredictInput) -> Result<Array1<f64>, EstimationError> {
let eta_mu = self.eta_mean(input);
let mean = self.strategy().inverse_link_array(eta_mu.view())?;
let precision = self.precision(input)?;
if mean.len() != precision.len() {
return Err(EstimationError::InvalidInput(format!(
"dispersion location-scale mean/precision length mismatch: {} vs {}",
mean.len(),
precision.len()
)));
}
let response = &self.likelihood.response;
let variance = Array1::from_shape_fn(mean.len(), |i| {
let mu = mean[i];
let prec = precision[i];
let var = match response {
ResponseFamily::NegativeBinomial { .. } => mu + mu * mu / prec,
ResponseFamily::Gamma => mu * mu / prec,
ResponseFamily::Beta { .. } => mu * (1.0 - mu) / (1.0 + prec),
ResponseFamily::Tweedie { p } => mu.powf(*p) / prec,
_ => 1.0 / prec,
};
var.max(0.0)
});
Ok(variance.mapv(f64::sqrt))
}
fn state_from_backend(
&self,
input: &PredictInput,
backend: &PredictionCovarianceBackend<'_>,
) -> Result<(Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>), EstimationError> {
let eta = self.eta_mean(input);
let strategy = self.strategy();
let (mean, dmu_deta) = inverse_link_mean_and_d1(&strategy, eta.view())?;
let p_d = self.beta_noise.len();
let eta_se = padded_design_standard_errors_from_backend(
&input.design,
backend,
0,
p_d,
"dispersion location-scale",
)?;
let mean_se = delta_method_mean_se_from_d1(&dmu_deta, &eta_se);
Ok((eta, mean, eta_se, mean_se))
}
}
impl PredictionTransform for DispersionLocationScalePredictor {
fn point_state(&self, input: &PredictInput) -> Result<LinearState, EstimationError> {
let eta = self.eta_mean(input);
let mean = self.strategy().inverse_link_array(eta.view())?;
let (eta_se, mean_se) = if let Some(covariance) = self.covariance.as_ref() {
let backend = PredictionCovarianceBackend::from_dense(covariance.view());
let (_, _, eta_se, mean_se) = self.state_from_backend(input, &backend)?;
(Some(eta_se), Some(mean_se))
} else {
(None, None)
};
Ok(LinearState {
eta,
mean,
eta_se,
mean_se,
covariance_corrected_used: false,
})
}
fn linear_state(
&self,
input: &PredictInput,
fit: &UnifiedFitResult,
pass: PredictPass,
covariance_mode: InferenceCovarianceMode,
) -> Result<LinearState, EstimationError> {
let p_total = self.beta_mu.len() + self.beta_noise.len();
let (backend, covariance_corrected_used) = match pass {
PredictPass::FullUncertainty => fit.select_uncertainty_backend(
p_total,
covariance_mode,
"dispersion location-scale",
)?,
PredictPass::PosteriorMean => (
require_posterior_mean_backend(
fit,
self.covariance.as_ref(),
p_total,
"dispersion location-scale posterior mean",
)?,
false,
),
};
let (eta, plugin_mean, eta_se, mean_se) = self.state_from_backend(input, &backend)?;
let mean = match pass {
PredictPass::FullUncertainty => plugin_mean,
PredictPass::PosteriorMean => {
let strategy = self.strategy();
let quadctx = crate::quadrature::QuadratureContext::new();
eta.iter()
.zip(eta_se.iter())
.map(|(&e, &se)| strategy.posterior_mean(&quadctx, e, se))
.collect::<Result<Array1<f64>, _>>()?
}
};
Ok(LinearState {
eta,
mean,
eta_se: Some(eta_se),
mean_se: Some(mean_se),
covariance_corrected_used,
})
}
fn response(&self, eta: &Array1<f64>) -> Result<Array1<f64>, EstimationError> {
self.strategy().inverse_link_array(eta.view())
}
fn response_jacobian_rows(&self, pass: PredictPass) -> ResponseInterval {
match pass {
PredictPass::FullUncertainty => ResponseInterval::SymmetricDelta,
PredictPass::PosteriorMean => ResponseInterval::TransformEta,
}
}
fn bounds(&self) -> ResponseBounds {
ResponseBounds::for_family(&self.likelihood.response)
}
fn response_family(&self) -> ResponseFamily {
self.likelihood.response.clone()
}
fn observation_noise(
&self,
input: &PredictInput,
) -> Result<Option<Array1<f64>>, EstimationError> {
self.noise_sd(input).map(Some)
}
}
impl PredictableModel for DispersionLocationScalePredictor {
fn predict_plugin_response(
&self,
input: &PredictInput,
) -> Result<PredictResult, EstimationError> {
let eta = self.eta_mean(input);
let mean = self.strategy().inverse_link_array(eta.view())?;
Ok(PredictResult { eta, mean })
}
fn predict_with_uncertainty(
&self,
input: &PredictInput,
) -> Result<PredictionWithSE, EstimationError> {
predict_with_uncertainty_generic(self, input)
}
fn predict_noise_scale(
&self,
input: &PredictInput,
) -> Result<Option<Array1<f64>>, EstimationError> {
self.noise_sd(input).map(Some)
}
fn predict_dispersion_scale(
&self,
input: &PredictInput,
) -> Result<Option<Array1<f64>>, EstimationError> {
let precision = self.precision(input)?;
let dispersion = match self.likelihood.response {
ResponseFamily::Tweedie { .. } => precision.mapv(|pr| 1.0 / pr.max(f64::MIN_POSITIVE)),
_ => precision,
};
Ok(Some(dispersion))
}
fn predict_full_uncertainty(
&self,
input: &PredictInput,
fit: &UnifiedFitResult,
options: &PredictUncertaintyOptions,
) -> Result<PredictUncertaintyResult, EstimationError> {
predict_full_uncertainty_generic(self, input, fit, options)
}
fn predict_posterior_mean(
&self,
input: &PredictInput,
fit: &UnifiedFitResult,
options: &PosteriorMeanOptions,
) -> Result<PredictPosteriorMeanResult, EstimationError> {
predict_posterior_mean_generic(self, input, fit, options)
}
fn n_blocks(&self) -> usize {
2
}
fn block_roles(&self) -> Vec<BlockRole> {
vec![BlockRole::Location, BlockRole::Scale]
}
}