Skip to contents

The following code in the Stan probabilistic programming language defines a model where the discrete species abundance, \(y\), follows a zero-inflated negative binomial distribution with probability of excess-zero and mean abundance both linked to the data through modskurt mean functions of the environmental gradient, \(x\).

functions {
  /*
  The "mskt" unimodal mean function
  @param xms ( predictor variable (real number vector) - mode (m, real number) ) 
              / scale (s, real number > 0)
  @param H height of curve at mode (real number > 0)
  @param r asymmetry (real number in (0, 1))
  @param d flatness (real number >= 0)
  @param p tail exaggeration (real number in (0.05, 1.95))
  @return vector of mean values (real number vector > 0)
  */
  vector mskt(vector xms, real H, real r, real d, real p) {
    real invp = 1 / p;
    real mdop = - d * invp;
    return fma(H,
               pow(fma(r, exp(fma(invp, xms / r, mdop)),
                       fma(1 - r, exp(fma(-invp, xms / (1 - r), mdop)),
                           - exp(mdop) + 1)),
                   -p),
               // avoid underflow for really small mu
               machine_precision());
    // unintuitively log scale creates more arithmetic precision issues
    // return log(H) - lmultiply(p, fma(
    //   r,
    //   exp( (xms ./ r - d) / p ),
    //   fma(
    //     1 - r,
    //     exp( -(xms ./ (1 - r) + d) / p ),
    //     - exp( -d / p ) + 1  
    //   )
    // ));
  }
  /*
  The "zinbl" zero-inflation link function
  @param mu mean negative binomial abundance (real number vector)
  @param H height of curve at mode (real number > 0)
  @param g0 log-odds probability of excess zero when mu = 0
  @param g1 rate at which log-odds probability of excess zero decreases as mu increases
  @return vector of excess zero probabilities (real number vector in [0, 1])
  */
  vector zilink(vector mu, real H, real g0, real g1) {
    return inv_logit(fma(-g1 / H, mu, g0));
  }
  real std_halfnormal_rng() {
    real u = uniform_rng(0.5, 1);
    real y = inv_Phi(u);
    return y;
  }
}
data {
  int<lower=1> N;
  int<lower=1> Nz;
  vector[N] x;

  // effort (can be 1 if no offset required)
  vector[N] eff;

  // make predictions over grid
  int Nrep;
  vector[Nrep] xrep;

  // re-scaling
  real<lower=0> y_max;
  real<lower=0> x_pos_range;
  real x_pos_min;

  // hyperpriors
  vector[2] hp_H;
  vector[2] hp_m;
  vector[2] hp_s;
  vector[2] hp_r;
  vector[2] hp_d;
  vector[2] hp_p;

  // toggle optional vars H,m,s[,r,d,p,a]
  int<lower=0,upper=1> use_r;
  int<lower=0,upper=1> use_d;
  int<lower=0,upper=1> use_p;

  // specify fixed values for parameters if not being used^
  real<lower=0,upper=1>   fix_r;
  real<lower=0>           fix_d;
  real<lower=0.1>         fix_p;

  // 0: no prior, 1: with prior, 2: only prior
  int<lower=0,upper=2> sample_prior;
  // integer abundance y 
  array[N] int<lower=0> y;
  real<lower=0> hp_kap;

  int<lower=0,upper=1> use_zi;
  vector[2] hp_g0;
  vector[1] hp_g1;
}
transformed data {
  int Nzp = Nz + 1;
  real exp_hp_s1 = exp(hp_s[1]);
}
parameters {
  real<lower=machine_precision(),upper=1> zH;
  real<lower=0,upper=1>                   zm;
  real<lower=machine_precision()>         zs;
  array[use_r] real<lower=machine_precision(),upper=1-machine_precision()> zr;
  array[use_d] real                                                        zd;
  array[use_p] real<lower=0,upper=1>                                       zp;
  array[use_zi] real zg0;
  array[use_zi] real<lower=0> zg1;
  // hopefully that upper bound restricts phi >= eps and avoids
  // underflow complaints MH tries kap -> inf
  real<lower=machine_precision(),upper=1/sqrt(machine_precision())> kap;
}
transformed parameters {
  // extract z rv's and get on desired scale
  // fma => hp_sd * z + hp_mu
  // https://mc-stan.org/docs/stan-users-guide/reparameterizations.html
  real H = zH * y_max;
  real m = fma(x_pos_range, zm, x_pos_min);
  // this can sometimes reach zero...
  // a better solution might be to sample zs from half-normal
  // then use expm1? 
  // real s = exp(fma(hp_s[2], zs, hp_s[1])) * x_pos_range;
  real s = exp_hp_s1 * expm1(hp_s[2] * zs) * x_pos_range;
  real r;
  if (use_r > 0) {
    r = zr[1];
  } else {
    r = fix_r;
  }
  real d;
  if (use_d > 0) {
    d = exp(fma(hp_d[2], zd[1], hp_d[1]));
  } else {
    d = fix_d;
  }
  real p;
  if (use_p > 0) {
    p = fma(1.95, zp[1], + 0.05);
  } else {
    p = fix_p;
  }
  vector[N] mu = eff .* mskt((x - m) / s, H, r, d, p);
  vector[N] lmu = log(mu);
  // for (n in 1:N) {
  //   if (is_nan(lmu[n])) {
  //     print("eff:", eff[n],
  //           ",x:", x[n],
  //           ",H:", H, 
  //           ",m:", m,
  //           ",s:", s, 
  //           ",r:", r, 
  //           ",d:", d, 
  //           ",p:", p);
  //   }
  // }
  // more numerically stable than pow(kap, -2)?
  real phi = 1 / square(kap);
  array[use_zi] real g0;
  array[use_zi] real g1;
  vector[N] zi;
  if (use_zi) {
    // logistic function of the relative mean
    g0[1] = fma(hp_g0[2], zg0[1], hp_g0[1]);
    g1[1] = hp_g1[1] * zg1[1];
    zi = inv_logit(fma(-g1[1] / H, mu, g0[1]));
  }
}
model {
  target += beta_lupdf(zH | hp_H[1], hp_H[2]);
  target += beta_lupdf(zm | hp_m[1], hp_m[2]);
  target += std_normal_lupdf(zs);
  if (use_r > 0) {
    target += beta_lupdf(zr[1] | hp_r[1], hp_r[2]);
  }
  if (use_d > 0) {
    target += std_normal_lupdf(zd[1]);
  }
  if (use_p > 0) {
    target += beta_lupdf(zp[1] | hp_p[1], hp_p[2]);
  }
  target += exponential_lupdf(kap | hp_kap);
  if (use_zi) {
    target += std_normal_lupdf(g0[1]);
    target += std_normal_lupdf(g1[1]);
    if (Nz > 0) {
      target += log_sum_exp(bernoulli_lupmf(1 | zi[1:Nz]),
                            bernoulli_lupmf(0 | zi[1:Nz]) +
                            neg_binomial_2_log_lupmf(0 | lmu[1:Nz], phi));
    }
    target += bernoulli_lupmf(0 | zi[Nzp:N]) +
              neg_binomial_2_log_lupmf(y[Nzp:N] | lmu[Nzp:N], phi);
  } else {
    target += neg_binomial_2_log_lupmf(y | lmu, phi);
  }
}
generated quantities {
  vector[N] log_lik;
  vector[Nrep] mu_rep;
  vector[Nrep] pr_mu_rep;
  real pr_H;
  real pr_m;
  real pr_s;
  real pr_r;
  real pr_d;
  real pr_p;
  real log_prior = 0;
  real pr_g0;
  real pr_g1;
  vector[Nrep] zi_rep;
  vector[Nrep] pr_zi_rep;
  array[N] int y_rep;
  array[N] int pr_y_rep;

  real pr_kap;
  real pr_phi;

  if (sample_prior < 2) {
    mu_rep = mskt((xrep - m) / s, H, r, d, p);
    // calc log lik and yreps
    if (use_zi) {
      zi_rep = inv_logit(fma(-g1[1] / H, mu_rep, g0[1]));
      for (n in 1:N) {
        if (n < Nzp) {
          log_lik[n] = log_sum_exp(bernoulli_lpmf(1 | zi[n]),
                                   bernoulli_lpmf(0 | zi[n]) +
                                   neg_binomial_2_log_lpmf(0 | lmu[n], phi));
        } else {
          log_lik[n] = bernoulli_lpmf(0 | zi[n]) +
                       neg_binomial_2_log_lpmf(y[n] | lmu[n], phi);
        }
        if (zi[n] > machine_precision() && bernoulli_rng(zi[n]) > 0.5) {
          y_rep[n] = 0;
        } else {
          y_rep[n] = neg_binomial_2_log_rng(lmu[n], phi);
        }
      }
    } else {
      for (n in 1:N) {
        log_lik[n] = neg_binomial_2_log_lpmf(y[n] | lmu[n], phi);
        if (mu[n] == 0) {
          y_rep[n] = 0;
        } else {
          y_rep[n] = neg_binomial_2_rng(mu[n], phi);
        }
      }
    }
  }

  if (sample_prior > 0) {
    // calcs log_prior for mean pars now
    // dont store these
    vector[N] pr_mu;

    // generate pars first for log_prior then transform
    real pr_zH = beta_rng(hp_H[1], hp_H[2]);
    log_prior += beta_lpdf(pr_zH | hp_H[1], hp_H[2]);
    pr_H = pr_zH * y_max;
    
    real pr_zm = beta_rng(hp_m[1], hp_m[2]);
    log_prior += beta_lpdf(pr_zm | hp_m[1], hp_m[2]);
    pr_m = fma(x_pos_range, pr_zm, x_pos_min);

    real pr_zs = std_normal_rng();
    log_prior += std_normal_lpdf(pr_zs);
    pr_s = exp(fma(hp_s[2], pr_zs, hp_s[1])) * x_pos_range;
    
    if (use_r > 0) {
      pr_r = beta_rng(hp_r[1], hp_r[2]);
      log_prior += beta_lpdf(pr_r | hp_r[1], hp_r[2]);
    } else {
      pr_r = fix_r;
    }
    
    if (use_d > 0) {
      real pr_zd = std_normal_rng();
      log_prior += std_normal_lpdf(pr_zd);
      pr_d = exp(fma(hp_d[2], pr_zd, hp_d[1]));
    } else {
      pr_d = fix_d;
    }
    
    if (use_p > 0) {
      real pr_zp = beta_rng(hp_p[1], hp_p[2]);
      log_prior += beta_lpdf(pr_zp | hp_p[1], hp_p[2]);
      pr_p = fma(1.95, pr_zp, 0.05);
    } else {
      pr_p = fix_p;
    }
    
    // maybe we can just calc this and derive the evenly gridded versions after?
    pr_mu = eff .* mskt((x - pr_m) / pr_s, pr_H, pr_r, pr_d, pr_p);
    pr_mu_rep = mskt((xrep - pr_m) / pr_s, pr_H, pr_r, pr_d, pr_p);
    // calc zi & yreps
    vector[N] pr_zi;
    if (use_zi) {
      pr_g0 = fma(hp_g0[2], std_normal_rng(), hp_g0[1]);
      pr_g1 = hp_g1[1] * std_halfnormal_rng();
      pr_zi = inv_logit(fma(-pr_g1 / pr_H, pr_mu, pr_g0));
      pr_zi_rep = inv_logit(fma(-pr_g1 / pr_H, pr_mu_rep, pr_g0));
    }
    pr_kap = exponential_rng(hp_kap);
    pr_phi = pow(pr_kap, -2);
    for (n in 1:N) {
      if (is_nan(pr_mu[n])) {
        pr_y_rep[n] = -999;
      } else if (pr_mu[n] <= machine_precision() ||
          (!is_nan(pr_zi[n]) &&
           pr_zi[n] > machine_precision() &&
           bernoulli_rng(pr_zi[n]) > 0.5)) {
        pr_y_rep[n] = 0;
      } else {
        pr_y_rep[n] = neg_binomial_2_rng(pr_mu[n], pr_phi);
      }
    }
  }
}