Random walk with positive differences and missing data

I’m trying to modify a random walk model with exponential errors and missing observations to have differences that are also constrained to be positive. I’m struggling with the model because there’s something that feels circular about the combination of missing data and the constraints, every time I try to push down one part, another part pops up. I think there’s some other way to think about it that I’m missing.

I had previously asked for help with a normal random walk in a question here Trouble fitting random walk plus exponential error . The motivation is the same, there are two manufacturing lines that produce products sequentially (the sequential IDs are called ‘relic numbers’ in their terminology, so that’s what’s in the code below). I want to estimate the production rate and how it changes over time to forecast the end of the production run, but I can only observe some delivery dates and no production dates. The errors are positive and have a long tail (since they’re essentially shipping times), and the previous normal + exponential random walk worked well for individual lines.

Where it failed was, I’m modeling the production rate as time between sequential products, and the larger goal is that lines A and B make different products right now, but when A finishes its run it will switch to the same product as B and I really want to forecast the production time for the end of the run of B. My plan was to add the rates together as 1/(1/A + 1/B) = AB/A+B using sampled differences instead of having to work out the distribution of that (I spent a little time on that but this seems better) but when the differences can be negative as in the previous normal random walk, that falls apart, so I’d like to constrain them to be positive.

One problem may be that the production time has to be below the observed delivery time, but when there are long gaps between observations, the production differences overshoot the next observation. But I don’t know how to determine if that’s causing the issue, and the constraints from the previous model don’t work the same when when I’m declaring a parameter for the differences.

I changed the previous model to make the random walk differences production_diff a parameter directly so it can get the <lower=0> constraint. But then I have to write the transformed production vector so that it uses the difference, but then I don’t know how to make sure those values are below the delivery_obs values since the upper is really delivery - prev production. The next thing I’m going try is splitting up production_diff into observed and unobserved. I tried working with lookups for whether each index is observed or unobserved and which index of that respective vector it was from, then building the production vector as either prev+diff or observed-error, but I couldn’t get that to work either.

Here’s where I’ve landed so far. Lots of ‘rejecting initial value’ errors like this, which makes me I think I’m looking in the right place for the problem, but I don’t know how to fix it

Chain 1: Rejecting initial value:
Chain 1:   Error evaluating the log probability at the initial value.
Chain 1: Exception: exponential_lpdf: Random variable[5] is -0.9674, but must be nonnegative! (in 'string', line 29, column 2 to column 66)

And here’s the model

data {
  int<lower=0> N;
  int<lower=0> N_obs;
  array[N_obs]   int<lower=1> relic_obs;
  array[N-N_obs] int<lower=1> relic_unobs;
  vector[N_obs] delivery_obs;
}
parameters {
  real prod_delay;
  real prod_mu;
  real<lower=0> prod_sigma;
  real<lower=0> ship_lambda;
  
  vector<lower=0>[N-1] production_diff;
}

transformed parameters {
  vector[N] production;
  production[1] = prod_delay;
  for (i in 2:N) {
    production[i] = production[i-1] + production_diff[i-1];
  }
}

model {
  prod_delay  ~ normal(20, 5);
  prod_mu     ~ normal(0, 1);
  prod_sigma  ~ normal(0, 2);
  ship_lambda ~ normal(0, 0.5);

  production_diff ~ lognormal(prod_mu, prod_sigma);
  delivery_obs - production[relic_obs] ~ exponential(ship_lambda);
}

And some code for generating simulated to pass to rstan

ex_data <- 
  tibble(
    relic = 1:200
  ) %>%
  mutate(prod_delay = 20,
         prod_error = rlnorm(n = n(), meanlog = log(0.5), sdlog = 1),
         prod = prod_delay + cumsum(prod_error),
         ship_error = rexp(n = n(), rate = 1/7),
         deliv = prod + ship_error,
         is_observed = row_number() %in% sample(1:150, size = 35, replace = FALSE))

ex_data %>%
  ggplot(aes(x = relic)) +
  geom_point(data = . %>% filter(!is_observed),
             aes(y = deliv, color = is_observed)) +
  geom_line(data = . %>% filter(relic <= 150),
            aes(y = prod)) +
  geom_point(data = . %>% filter(is_observed),
             aes(y = deliv, color = is_observed)) +
  scale_color_manual(guide = "none",
                     values = c("TRUE"= "#000000", "FALSE" = "#CCCCCC"))

stan_data <- list(
  N = nrow(ex_data),
  N_obs = sum(ex_data$is_observed),
  relic_obs = ex_data$relic[ex_data$is_observed],
  relic_unobs = ex_data$relic[!ex_data$is_observed],
  delivery_obs = ex_data$deliv[ex_data$is_observed]
)

You need to ensure that production[relic_obs] is never greater than the corresponding delivery_obs because the exponential distribution only has positive support.

Ok, so it sounds like I was headed in the right direction trying to split up the production differences into observed and unobserved so I can put an upper constraint on the observed one. It also seems helpful to put a similar constraint on the unobserved one.

I didn’t see a way to put the constraint I need directly on production_diff because I think it needs to be the difference between the observed delivery time and the previous production time for observed, which depends on production_diff, so the only thing I could come up from reading around with was constraining it to [0,1] then transforming it based on that difference later.

To make sure the definition of production isn’t circular, I did a bunch of stuff in the transformed parameters block to construct it sequentially out of those differences for observed and unobserved, using a kind of reverse lookup for each index of everything to the respective index of observed or unobserved vectors.

data {
  int<lower=0> N;
  int<lower=0> N_obs;
  array[N_obs]   int<lower=1> relic_obs;
  array[N-N_obs] int<lower=1> relic_unobs;
  vector[N_obs] delivery_obs;
}

transformed data {
  // arary indicating whether index is observed
  array[N] int<lower=0, upper=1> is_obs = rep_array(0, N);
  for (i in 1:N_obs) {
    is_obs[relic_obs[i]] = 1;
  }
  // reverse lookups for indices
  array[N] int<lower=1, upper=N> relic_index;
  for (i in 1:N_obs) {
    relic_index[relic_obs[i]] = i;
  }
  for (i in 1:(N-N_obs)) {
    relic_index[relic_unobs[i]] = i;
  }
  
  // first observed delivery time after each unobserved relic
  vector[N-N_obs] delivery_next_after_unobs;
  for (i in 1:(N-N_obs)) {
    for (j in (relic_unobs[i]+1):N) {
      if (is_obs[j] == 1) {
        delivery_next_after_unobs[i] = delivery_obs[relic_index[j]];
        break;
      }
    }
  }
}

parameters {
  real prod_delay;
  real prod_mu;
  real<lower=0> prod_sigma;
  real<lower=0> ship_lambda;
  
  vector<lower=0, upper=1>[N_obs] production_diff_obs_unit;
  vector<lower=0, upper=1>[N-N_obs] production_diff_unobs_unit;
}

transformed parameters {
  vector[N_obs] production_diff_obs_mult;
  vector[N-N_obs] production_diff_unobs_mult;
  vector<lower=0>[N-1] production_diff;
  vector[N] production;

  // have to do it in a for loop cause everything depends on previous
  production[1] = prod_delay;
  if (relic_obs[1] == 1) {
    production_diff_obs_mult[1] = 0;
  }
  if (relic_unobs[1] == 1) {
    production_diff_unobs_mult[1] = 0;
  }
  for (i in 2:N) {
    // unobserved diffs can't jump above the next observed delivery
    // observed diffs can't jump above the current delivery
    if (is_obs[i] == 0) {
      production_diff_unobs_mult[relic_index[i]] = delivery_next_after_unobs[relic_index[i]] - production[i-1];
      production_diff[i-1] = production_diff_unobs_unit[relic_index[i]] * production_diff_unobs_mult[relic_index[i]];
    } else {
      production_diff_obs_mult[relic_index[i]] = delivery_obs[relic_index[i]] - production[i-1];
      production_diff[i-1] = production_diff_obs_unit[relic_index[i]] * production_diff_obs_mult[relic_index[i]];
    }

    production[i] = production[i-1] + production_diff[i-1];
  }
}

model {
  prod_delay  ~ normal(20, 5);
  prod_mu     ~ normal(0, 1);
  prod_sigma  ~ normal(0, 2);
  ship_lambda ~ normal(0, 0.5);
  
  for (i in 2:N) {
    if (is_obs[i] == 0) {
      if (production_diff_unobs_mult[relic_index[i]] > 0) {
        target += log(fabs(production_diff_unobs_mult[relic_index[i]]));
      } else {
        target += negative_infinity();
      }
    }
    if (is_obs[i] == 1) {
      if (production_diff_obs_mult[relic_index[i]] > 0) {
        target += log(fabs(production_diff_obs_mult[relic_index[i]]));
      } else {
        target += negative_infinity();
      }
    }
  }

  production_diff ~ lognormal(prod_mu, prod_sigma);
  delivery_obs - production[relic_obs] ~ exponential(ship_lambda);
}

But I’m still getting similar rejecting initial values errors evaluating the log probability on some production_diff index.
I’m going to keep thinking about it, but is there a better way to set constraints that depend on previous values and data like this? The circular definition seemed like the first problem, so I wrote out the whole sequence as a loop, then the dynamic constraints in the parameters block were circular, so I searched around and saw some examples doing this multiplier thing, but I couldn’t think of anything else. I naively hoped this would be as easy as change normal to lognormal and give it the right parameters :)

I’d approach as follows. It’s a bit confusing because I’ve left the unobserved and observed vectors separate but you could refactor a lot of this. Notes

  • You need to have a lower bound on the production delay because it doesn’t make sense for that to be negative.
  • A lognormal prior on production_diff doesn’t follow your data generating process coded in R as the difference between productions seemingly can be negative
  • you have to handle the case when the first index is observed vs unobserved
  • I think it makes more sense to model the latent unobserved delivery to get the correct bounds

This samples but has issues. You’ll want to play around with the priors and make sure your data generating process is correct.

data {
  int<lower=0> N;
  int<lower=0> N_obs;
  array[N_obs]   int<lower=1> relic_obs;
  array[N-N_obs] int<lower=1> relic_unobs;
  vector[N_obs] delivery_obs;

  array[N] int<lower=0, upper=1> is_obs;
}
parameters {
  real<lower=(is_obs[1] == 1 ? delivery_obs[1] : 0)> prod_delay;   // prod_delay should be >= 0
  real prod_mu;
  real<lower=0> prod_sigma;
  real<lower=0> ship_lambda;
  
  vector[N - N_obs] delivery_unobs_raw;
  vector[N - 1] production_diff_raw;
}

transformed parameters {
  vector[N] production;
  vector[N - 1] production_diff;
  vector[N - N_obs] delivery_unobs;
  vector[N] delivery;

    production[1] = prod_delay;

  {
    int obs_cnt = 1;
    int unobs_cnt = 1;

    if (is_obs[1] == 1) {
    delivery[1] = delivery_obs[1];
  } else {
    delivery_unobs[1] = lower_bound_jacobian(delivery_unobs_raw[1], prod_delay);
    delivery[1] = delivery_unobs[unobs_cnt];
    unobs_cnt += 1;
  }

  for (i in 2:N) {
    if (is_obs[i] == 1) {
        production[i] = lower_upper_bound_jacobian(production_diff_raw[i - 1], delivery_obs[obs_cnt] - production[i - 1],  delivery_obs[obs_cnt]);
        obs_cnt += 1;
    } else {
        delivery_unobs[unobs_cnt] = lower_bound_jacobian(delivery_unobs_raw[unobs_cnt], production[i - 1]);
        production[i] = lower_upper_bound_jacobian(production_diff_raw[i - 1], delivery_unobs[unobs_cnt] - production[i - 1], delivery_unobs[unobs_cnt]);
        unobs_cnt += 1;
    }
  }
  production_diff = production[2:N] - production[1:N - 1];
}
}

model {
  prod_delay  ~ normal(20, 5);
  prod_mu     ~ normal(0, 1);
  prod_sigma  ~ normal(0, 2);
  ship_lambda ~ normal(0, 0.5);

  production_diff ~ normal(prod_mu, prod_sigma);
  delivery_obs - production[relic_obs] ~ exponential(ship_lambda);
  delivery_unobs - production[relic_unobs] ~ exponential(ship_lambda);
}

Using Claude with the previous model and asking it to reparameterize the random walk to avoid the sampling issues gives a performant and cleaner model:

data {
  int<lower=0> N;
  int<lower=0> N_obs;
  array[N_obs]     int<lower=1> relic_obs;
  array[N - N_obs] int<lower=1> relic_unobs;
  vector[N_obs] delivery_obs;

  array[N] int<lower=0, upper=1> is_obs;
}

transformed data {
  int N_unobs = N - N_obs;
  vector[N] prod_ub = rep_vector(positive_infinity(), N);
  for (k in 1:N_obs) {
    prod_ub[relic_obs[k]] = delivery_obs[k];
  }
}

parameters {
  real prod_mu;
  real<lower=0> prod_sigma;
  real<lower=0> ship_lambda;

  real prod_delay_raw;              // std-normal base for production[1]
  vector[N - 1] step_raw;           // per-step base (std-normal if unobserved,
                                    //  upper-bound base if observed)
  vector<lower=0>[N_unobs] gap_unobs;  // shipping gaps at unobserved relics (>0)
}

transformed parameters {
  vector[N] production;

  if (is_obs[1] == 1) {
    production[1] = upper_bound_jacobian(prod_delay_raw, delivery_obs[1]);
  } else {
    production[1] = 20 + 5 * prod_delay_raw;   // non-centered
  }

  for (i in 2:N) {
    if (is_obs[i] == 1) {
      production[i] = upper_bound_jacobian(step_raw[i - 1], prod_ub[i]);
    } else {
      production[i] = production[i - 1] + prod_mu + prod_sigma * step_raw[i - 1];
    }
  }
}

model {
  prod_mu     ~ normal(0, 1);
  prod_sigma  ~ normal(0, 2);
  ship_lambda ~ normal(0, 0.5);

  if (is_obs[1] == 1) {
    production[1] ~ normal(20, 5);         
  } else {
    prod_delay_raw ~ std_normal();    
  }

  for (i in 2:N) {
    if (is_obs[i] == 1) {
      production[i] - production[i - 1] ~ normal(prod_mu, prod_sigma);
    } else {
      step_raw[i - 1] ~ std_normal();                                
    }
  }

  delivery_obs - production[relic_obs] ~ exponential(ship_lambda);
  gap_unobs ~ exponential(ship_lambda);
}

generated quantities {
  vector[N_unobs] delivery_unobs = production[relic_unobs] + gap_unobs;
  vector[N - 1] production_diff = production[2:N] - production[1:N - 1];
}

I should have some time to return to this over the next week and I’m going to keep trying to apply these suggestions, thanks for looking at it. The upper_bound_jacobian function was helpful and new to me (and forced me to learn a little about rstan vs cmdstanr)

As far as lognormal production_diff, I had changed the simulated data to have lognormal differences, but I’ll look at that again, maybe I’m missing something.
The motivation is, one extension of this forecast involves fitting this for two different production lines and when one finishes they’ll start working together. So I want to eventually combine the rates and I thought constraining the differences to be positive would be a reasonable way to keep that combined rate from behaving strangely

I worked through the reparameterized model you posted and it seems do fine and is maybe samples faster than the previous normal differences model that was working, so now I’m trying to understand what it would take to modify this for other distributions of differences. I’m seeing error evaluating log probability from lognormal_lpdf as before. I think tomorrow I’ll think harder about initial values, then maybe try it with some data where there aren’t long stretches of unobserved data cause my (inexperienced) best guess is that it’s having trouble with production differences overshooting the next known upper bound

The lognormal also makes the errors proportional to the value rather than additive with a fixed scale. Otherwise, you could use a half normal.

The lognormal requires a positive outcome. And also, the location parameter is on the log scale (and it’s not a mean because of skew, but exp(scale) will be the median).