1
$\begingroup$

The number of daily users ordering from an e-commerce can be modeled using a Poisson distribution.

I want to detect anomalies using some kind of hypothesis test or probabilistic reasoning. That is, I use the data of the last months to calculate the mean of my Poisson distribution. If a given day I observe a number of orders way lower than the mean of my distribution, then I reject the null hypothesis of the number of orders of that day being generated by the Poisson fitted with the data of the last months.

Can you help me formalize this? What exact test would you run for this purpose? How would you find the mean of the Poisson using data of the last months? Do you think some kind of bayesian alternative could make more sense?

$\endgroup$

1 Answer 1

3
$\begingroup$

I probably wouldn't do this with a hypothesis test, but we can use some probabilistic reasoning here.

Poisson regression is a way to sort of "fit" a distribution on various covariates (like day of week for example). So here is what I propose.

  • Use a couple months worth of data to fit a poisson regression
  • Estimate prediction intervals via the bootstrap, and
  • When an observation falls outside of these intervals, declare it as anomalous.

I'll illustrate with a simulated example. Suppose you're working with data that looks like the plot below. The data runs between Oct 1 2023 and Dec 31 2023, and let's say you're looking to make predictions on December.. Clearly, there is an anomaly sometime during December. I've purposefully made this very large for purposes of illustration.

Our first step is to fit some sort of model. There is clearly some sort of periodic behaviour here, so a good first attempt would be to fit a regression on day of week.

enter image description here

# Fit a model
training <- filter(d, dts<'2023-12-01')
fit <- glm(y ~ dow, data=training, family = poisson())

Now, we can plot our predictions to see how the model does. I'll focus just on December for now. The data (black) and predictions (red) look like

enter image description here

Predictions look good, but how can we tell if the errors in the predictions are normal versus anomalous. We need some sense of what is to be expected. Here is where we use the bootstrap to integrate out the uncertainty.

The sampling distribution of the coefficients is something like

$$\hat{\beta} \sim \mbox{Normal}(\beta, \Sigma)$$

R provides us with an estimate of $\Sigma$ via the vcov method, we can act as if $\beta = \hat{\beta}$ for now. So we can draw a bunch of samples from this distribution, compute our predictions, and sample from the likelihood to get an idea of what kind of observations we should expect to see.

The outcome is something like this

enter image description here

The red ribbon is the range where we expect to see 95% of observations from the process, assuming the model is correct. So one way to operationalize an outlier would be to say "if it is outside this band, then it has low probability of happening". As you can see, out outlier is above this point, so it would be deemed as an outlier.

There are various ways this could be improved (e.g. by bootstrapping the model fit within the computation of the prediction intervals), but I think this is fine for now.

Code

library(tidyverse)

# Create a dater range
dts <- seq(
  ymd('2023-10-01'),
  ymd('2023-12-31'),
  by='day'
)

d <- tibble(dts) %>% 
  mutate(
    dow = wday(dts, label=T)
  )

# Simulate the data
X <- model.matrix(~dow-1, data = d)
beta <- log(c(200, 300, 400, 400, 400, 400, 200))
y <- rpois(nrow(X), exp(X%*%beta))
y[80] <- round(y[80] * 1.5)
d$y <- y



plot <- d %>% 
        ggplot(aes(dts, y)) + 
        geom_line() + 
        theme(aspect.ratio = 1/1.61)


# Fit a model
training <- filter(d, dts<'2023-12-01')
fit <- glm(y ~ dow, data=training, family = poisson())


d$pred <- predict(fit, newdata = d, type='response')


plot +
  geom_line(data=d, aes(dts, pred), color='red') + 
  scale_x_date(limits = c(ymd('2023-12-01'), NA))


get_prediction_intervals <- function(fit){
  
  samples <- map_dfr(1:1000, ~{
    
  b <- coef(fit)
  Sigma <- vcov(fit)
  bsamp <- MASS::mvrnorm(n=1, mu=b, Sigma = Sigma)
  
  # Make predictions for each day of week
  Xd <- fit$data %>% 
        distinct(dow)
  
  lambda <- predict(fit, newdata = Xd, type = 'response')
  Xd$yobs <- rpois(length(lambda), lambda)
  
  Xd
  }, .id = 'sample')
  
  
  samples %>% 
    group_by(dow) %>% 
    reframe(
      prob = c(0.025, 0.975),
      bnd = c('pred.lo', 'pred.hi'),
      pct = quantile(yobs, probs = c(0.025, 0.975))
    ) %>% 
    pivot_wider(id_cols = c('dow'), names_from = 'bnd', values_from = 'pct')
  
}


ints <- get_prediction_intervals(fit=fit)



plot +
  geom_line(data=d, aes(dts, pred), color='red') + 
  geom_ribbon(data=left_join(d, ints), aes(dts, pred, ymin=pred.lo, ymax=pred.hi), alpha=0.5, fill='red', inherit.aes = F) +
  scale_x_date(limits = c(ymd('2023-12-01'), NA))

```
$\endgroup$
2
  • $\begingroup$ How de you get something like a p-value from this? I need to do multiple comoarison and a FDR analysis so a p-value would be very useful $\endgroup$ Commented Oct 8, 2023 at 6:29
  • $\begingroup$ @DavidMasip. You seem insistent on a p value even though your post says "* hypothesis test or probabilistic reasoning*". I've provided a probabilistic perspective which I think is much more useful than a hypothesis test. If you're looking for a single number to tell you if an observation is anomalous or not, you can construct indicators for if the observation lays outside the simulated prediction interval. $\endgroup$ Commented Oct 8, 2023 at 15:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.