Hi everyone
I am working on a measurement error model in variables for the validation of satellite-derived evapotranspiration (ET) estimates against ground-truth observations collected in perennial crop fields. Validating satellite ET products is inherently challenging because ground-based measurements, typically obtained from eddy covariance stations, are not error-free.
The model I adopt is a Bayesian Simple Linear Regression (SLR) with measurement error in both response and explanatory variables:
- Outcome model, relates observed satellite ET to the true latent ET via a Student-t likelihood, providing robustness to outliers.
- To account for inter-annual variability in the satellite-ground ET relationship, the model adopts a hierarchical structure over years (K = 3 years). All variables are standardized prior to fitting to improve sampler geometry, and a non-centered parameterization is used for all year-level random effects.
The Stan model is reported below:
data {
int<lower=0> N;
int<lower=0> K; // Number of years
array[N] int<lower=1, upper=K> year_id; // Year index for each observation
vector[N] x_meas; // Ground-station ET (observed, with error)
vector[N] y_obs; // Satellite ET (observed)
vector[N] sigma_u; // Known ground measurement error SD
real mean_x;
real sd_x;
real mean_y;
real sd_y;
}
transformed data {
vector[N] x_meas_std = (x_meas - mean_x) / sd_x;
vector[N] y_obs_std = (y_obs - mean_y) / sd_y;
vector[N] sigma_u_std = sigma_u / sd_x;
real slope_prior_center = sd_x / sd_y; // ~1 on standardised scale when x ~ y
}
parameters {
// ── Population-level hyperparameters ─────────────────────────────────
real mu_beta0; // population mean intercept
real<lower=0> tau_beta0; // population SD intercept
real mu_beta1; // population mean slope
real<lower=0> tau_beta1; // population SD slope
real mu_log_sigma_e; // log population mean of residual SD
real<lower=0> tau_log_sigma_e; // SD of log residual SD
real mu_log_sigma_x; // log population mean of latent ET SD
real<lower=0> tau_log_sigma_x; // SD of log latent ET SD
real<lower=2> nu; // Student-t degrees of freedom (shared)
// ── Year-level random effects (non-centered) ──────────────────────────
vector[K] z_beta0;
vector[K] z_beta1;
vector[K] z_log_sigma_e;
vector[K] z_log_sigma_x;
// ── Latent true ET ───────────────────────────────────────────────────
vector[N] x_true_raw; // std_normal auxiliary variable
}
transformed parameters {
// Year-specific parameters (non-centered reparameterisation)
vector[K] beta0_std = mu_beta0 + tau_beta0 * z_beta0;
vector[K] beta1_std = mu_beta1 + tau_beta1 * z_beta1;
vector[K] sigma_e_std = exp(mu_log_sigma_e + tau_log_sigma_e * z_log_sigma_e);
vector[K] sigma_x_std = exp(mu_log_sigma_x + tau_log_sigma_x * z_log_sigma_x);
// Latent true ET scaled by year-specific spread
vector[N] x_true_std;
for (i in 1:N)
x_true_std[i] = sigma_x_std[year_id[i]] * x_true_raw[i];
}
model {
// ── Hyperpriors (Gelman 2006; Vehtari et al. 2021) ───────────────────
// Intercept: centred at 0 on standardised scale
mu_beta0 ~ normal(0, 1.0);
tau_beta0 ~ normal(0, 0.2); // half-normal (lower=0 constraint)
// Slope: centred at slope_prior_center (~1 on std scale, expecting sat ≈ ground)
mu_beta1 ~ normal(slope_prior_center, 0.3);
tau_beta1 ~ normal(0, 0.1); // tight: slope not expected to vary strongly across years
// log(σ_e): log-scale prior, centred around log(0.5) ≈ -0.7 on std scale
mu_log_sigma_e ~ normal(-0.7, 0.7);
tau_log_sigma_e ~ normal(0, 0.15);
// log(σ_x): log-scale prior for latent ET spread
mu_log_sigma_x ~ normal(0, 0.7);
tau_log_sigma_x ~ normal(0, 0.15);
// ν (degrees of freedom): gamma(10, 0.5) → mean = 20; allows heavy tails
nu ~ gamma(10, 0.5);
// ── Non-centered auxiliary priors ────────────────────────────────────
z_beta0 ~ std_normal();
z_beta1 ~ std_normal();
z_log_sigma_e ~ std_normal();
z_log_sigma_x ~ std_normal();
x_true_raw ~ std_normal();
// ── Likelihood ────────────────────────────────────────────────────────
for (i in 1:N) {
// Measurement model: observed ground ET | true ET ~ Normal(x_true, σ_u²)
x_meas_std[i] ~ normal(x_true_std[i], sigma_u_std[i]);
// Outcome model: satellite ET | true ET ~ Student-t(ν, β₀ + β₁·x_true, σ_e)
y_obs_std[i] ~ student_t(nu, beta0_std[year_id[i]] + beta1_std[year_id[i]] * x_true_std[i],sigma_e_std[year_id[i]]);
}
}
generated quantities {
// ── Back-transform year-specific parameters to original scale ─────────
vector[K] beta_1;
vector[K] beta_0;
vector[K] sigma_e;
vector[K] sigma_x;
for (k in 1:K) {
beta_1[k] = beta1_std[k] * sd_y / sd_x;
beta_0[k] = mean_y - beta_1[k] * mean_x + beta0_std[k] * sd_y;
sigma_e[k] = sigma_e_std[k] * sd_y;
sigma_x[k] = sigma_x_std[k] * sd_x;
}
// ── Population-level parameters (original scale) ──────────────────────
real pop_beta_1 = mu_beta1 * sd_y / sd_x;
real pop_beta_0 = mean_y - pop_beta_1 * mean_x + mu_beta0 * sd_y;
real pop_sigma_e = exp(mu_log_sigma_e) * sd_y; // population median σ_e
real pop_sigma_x = exp(mu_log_sigma_x) * sd_x;
// ── Predictions ───────────────────────────────────────────
vector[N] y_pred;
vector[N] y_rep;
vector[N] log_lik;
for (i in 1:N) {
real x_orig = x_true_std[i] * sd_x + mean_x;
int k = year_id[i];
y_pred[i] = beta_0[k] + beta_1[k] * x_orig;
y_rep[i] = student_t_rng(nu, beta_0[k] + beta_1[k] * x_orig, sigma_e[k]);
log_lik[i] = student_t_lpdf(y_obs_std[i] | nu,
beta0_std[k] + beta1_std[k] * x_true_std[i],
sigma_e_std[k]);
}
}
Could anyone help me check whether the code is correct?Modeling hierarchical-model