---
title: "wsgnstat: the wrapped skew generalised normal distribution"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{wsgnstat: the wrapped skew generalised normal distribution}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4,
                      dpi = 72)
has_circular <- requireNamespace("circular", quietly = TRUE)
if (has_circular) {
  wind_env <- new.env()
  utils::data("wind", package = "circular", envir = wind_env)
}
```

```{r setup}
library(wsgnstat)
```

## The law and its parameters

On the line, the skew generalised normal distribution has the two-piece exponential-power density

$$
f(x) = c \, \exp\!\left\{-\left(\frac{\lambda_{\pm} |x - \mu|}{\sigma}\right)^{\beta}\right\},
\qquad
c = \frac{1}{\sigma \, (1/\lambda_1 + 1/\lambda_2) \, \Gamma(1 + 1/\beta)},
$$

with $\lambda_1$ to the right of the location $\mu$ and $\lambda_2$ to its left. It is the scale
mixture of a uniform variable and a generalised gamma variable, which is how `rsgn()` draws from it.
The shape $\beta > 0$ governs the tails ($\beta = 2$ is Gaussian, $\beta = 1$ Laplace-like, smaller
values heavier), and the pair $(\lambda_1, \lambda_2)$ the asymmetry: the mass to the left of $\mu$
is $\lambda_1 / (\lambda_1 + \lambda_2)$.

Wrapping onto the circle sums the line density over all turns,
$f_W(\theta) = \sum_k f(\theta + 2\pi k)$. On the circle the package uses the identified
parameterisation $\lambda_1 = \lambda$, $\lambda_2 = 1 - \lambda$ and
$\sigma = \sqrt{\lambda (1 - \lambda)}$, so the circular law has three parameters: the location
`mu`, the tail parameter `beta` and the skewness `lam`, with `lam = 0.5` symmetric. For a skew law
`mu` is the junction of the two pieces, not the mean direction.

```{r family, fig.cap = "Wrapped densities for three tail parameters, symmetric (solid) and skew (dashed)."}
th <- seq(0, 2 * pi, length.out = 721)
op <- par(mfrow = c(1, 3), mar = c(4, 4, 2, 1))
for (b in c(0.8, 1.5, 3)) {
  plot(th, dwsgn(th, mu = pi, beta = b, lam = 0.5), type = "l", ylim = c(0, 1.2),
       xlab = "angle (radians)", ylab = "density", main = paste("beta =", b))
  lines(th, dwsgn(th, mu = pi, beta = b, lam = 0.25), lty = 2)
}
par(op)
```

## Density, distribution function, quantiles and random generation

The four functions follow R's usual naming. The distribution function is measured anticlockwise
from angle zero and is computed exactly, as a sum over turns of incomplete gamma functions, so it
needs no numerical quadrature; the quantile function inverts it.

```{r dpqr}
mu <- 1
beta <- 1.5
lam <- 0.3
dwsgn(c(0.5, 1, 2), mu, beta, lam)
pwsgn(c(0.5, 1, 2), mu, beta, lam)
qwsgn(c(0.1, 0.5, 0.9), mu, beta, lam)
```

The distribution function agrees with direct numerical integration of the density, and the
density integrates to one over the circle:

```{r dpqr-check}
c(pwsgn = pwsgn(2, mu, beta, lam),
  integrate = integrate(dwsgn, 0, 2, mu = mu, beta = beta, lam = lam)$value,
  total = integrate(dwsgn, 0, 2 * pi, mu = mu, beta = beta, lam = lam)$value)
```

Draws in the identified parameterisation come from `rwsgn()`; `rsgn()` draws on the line, and
wraps if asked, for any $(\sigma, \lambda_1, \lambda_2)$.

```{r rwsgn, fig.cap = "A sample of 5000 draws with the wrapped density overlaid."}
set.seed(1)
x <- rwsgn(5000, mu, beta, lam)
hist(x, breaks = seq(0, 2 * pi, length.out = 49), freq = FALSE, main = "",
     xlab = "angle (radians)")
curve(dwsgn(x, mu, beta, lam), 0, 2 * pi, add = TRUE, lwd = 2)
```

A Kolmogorov-Smirnov distance between the sample and the exact distribution function confirms the
agreement:

```{r ks}
u <- sort(pwsgn(x, mu, beta, lam))
n <- length(u)
c(KS = max(pmax((1:n) / n - u, u - (0:(n - 1)) / n)), five_per_cent_critical = 1.358 / sqrt(n))
```

## Reductions to known circular laws

At $\beta = 2$ the line law is the two-piece (split) normal, so the circular law is the wrapped
split normal; with `lam = 0.5` as well, $\sigma = 1/2$ and the rate is one on both sides, so the
line law is $N(0, 1/2)$ and the circular law is the wrapped normal with $\sigma = 1/\sqrt{2}$.
Both reductions can be checked against densities built directly from `dnorm()`:

```{r wn}
tt <- c(0.5, 2, 4.5)
k <- -60:60
wrapped_normal <- vapply(tt, function(t) sum(dnorm(t + 2 * pi * k, 0, sqrt(1 / 2))), 0)
rbind(wsgnstat = dwsgn(tt, mu = 0, beta = 2, lam = 0.5), wrapped_normal = wrapped_normal)

## wrapped split normal: standard deviation sigma / (sqrt(2) lam) on the right of mu and
## sigma / (sqrt(2) (1 - lam)) on the left, with left mass lam
l <- 0.3
s <- sqrt(l * (1 - l))
split_normal <- function(x) {
  sr <- s / (sqrt(2) * l)
  sl <- s / (sqrt(2) * (1 - l))
  ifelse(x >= 0, 2 * (1 - l) * dnorm(x, 0, sr), 2 * l * dnorm(x, 0, sl))
}
wrapped_split <- vapply(tt, function(t) sum(split_normal(t + 2 * pi * k)), 0)
rbind(wsgnstat = dwsgn(tt, mu = 0, beta = 2, lam = l), wrapped_split_normal = wrapped_split)
```

The trigonometric moments of the wrapped normal are $\exp(-p^2 \sigma^2 / 2) = \exp(-p^2/4)$, and
the engine reproduces them through its characteristic function:

```{r tm}
tm <- trig_moments(4, mu = 0, beta = 2, lam = 0.5)
rbind(engine = tm$A, closed_form = exp(-(1:4)^2 / 4))
```

In the identified parameterisation the concentration of the symmetric members is tied to the tail
parameter, so the wrapped normal reached at $\beta = 2$ is the one with $\sigma = 1/\sqrt{2}$; the
skew members at $\beta = 2$ are wrapped split normals of the matching scale.

The wrapped skew-normal of Pewsey (2000) skews the normal differently, through the factor
$2\Phi(\alpha z)$ of Azzalini's construction rather than through two pieces. The two families
share the wrapped normal, at $\alpha = 0$ there and at $\beta = 2$, `lam = 0.5` here:

```{r wsn}
dwsn <- function(theta, xi, omega, alpha, k = -60:60) {
  vapply(theta, function(t) {
    z <- (t + 2 * pi * k - xi) / omega
    sum(2 / omega * dnorm(z) * pnorm(alpha * z))
  }, 0)
}
rbind(wrapped_skew_normal_alpha0 = dwsn(tt, 0, sqrt(1 / 2), 0),
      wsgnstat = dwsgn(tt, mu = 0, beta = 2, lam = 0.5))
```

Away from the wrapped normal the two skewing mechanisms give different shapes (two half-normals
joined at the mode, against a normal density reweighted by a normal distribution function), so
neither family contains the other's skew members.

## Estimation from simulated data

Two estimators are provided. `fit_wsgn_trig()` matches the first two sample trigonometric moments
to their population values (minimum distance, with sandwich standard errors); the population
moments come from the characteristic function, which is evaluated through absolutely convergent
integrals and is therefore available for every $\beta > 0$, including $\beta \le 1$, where the
Fox-Wright series representation does not converge everywhere. `fit_wsgn_mle()` maximises the
wrapped likelihood, starting from the moment estimate; its standard errors, when asked for, come
from a parametric bootstrap, because the observed information is unstable near the cusp the
density develops as $\beta$ approaches one.

```{r sim}
truth <- c(mu = 4, beta = 0.8, lam = 0.6)
set.seed(17)
y <- rwsgn(2000, truth["mu"], truth["beta"], truth["lam"])
ft <- fit_wsgn_trig(y)
fm <- fit_wsgn_mle(y)
round(rbind(truth = truth, moments = ft$par, moments_se = ft$se, likelihood = fm$par), 3)
```

The moment estimates lie within `r round(max(abs((ft$par - truth) / ft$se)), 1)` of their own
standard errors of the truth in every coordinate, and the likelihood estimates within
`r round(max(abs(fm$par - truth)), 3)` of it in every coordinate, at a heavy-tailed shape
($\beta = 0.8$) where the characteristic function is reached only through the integral route.
Bootstrap standard errors for the likelihood fit refit the model once per replicate; a small run
at a lighter-tailed member:

```{r boot}
truth2 <- c(mu = 1, beta = 1.5, lam = 0.3)
set.seed(18)
y2 <- rwsgn(500, truth2["mu"], truth2["beta"], truth2["lam"])
fb <- fit_wsgn_mle(y2, B = 30)
round(rbind(truth = truth2, estimate = fb$par, bootstrap_se = fb$se), 3)
```

## An application: wind directions at Col de la Roa

The `wind` data in the 'circular' package hold `r if (has_circular) length(wind_env$wind) else 310`
wind directions, in radians, recorded at a meteorological station in the Italian Alps between
January and March 2001 (Agostinelli 2007). This section runs only when 'circular' is installed.

```{r wind, eval = has_circular}
w <- as.numeric(wind_env$wind) %% (2 * pi)
fw_t <- fit_wsgn_trig(w)
fw <- fit_wsgn_mle(w)
round(rbind(moments = fw_t$par, moments_se = fw_t$se, likelihood = fw$par), 3)
```

For comparison, the wrapped normal and wrapped Cauchy laws are fitted by maximum likelihood with
the 'circular' package, and the three fits are set side by side by log-likelihood and AIC.

```{r wind-compare, eval = has_circular}
wc <- circular::circular(w, type = "angles", units = "radians", template = "none",
                         modulo = "asis", zero = 0, rotation = "counter")
f_wn <- circular::mle.wrappednormal(wc)
f_wc <- circular::mle.wrappedcauchy(wc)
ll <- c(wsgn = fw$logLik,
        wrapped_normal = sum(log(circular::dwrappednormal(wc, f_wn$mu, f_wn$rho))),
        wrapped_cauchy = sum(log(circular::dwrappedcauchy(wc, f_wc$mu, f_wc$rho))))
npar <- c(3, 2, 2)
round(cbind(logLik = ll, parameters = npar, AIC = -2 * ll + 2 * npar), 1)
```

```{r wind-plot, eval = has_circular, fig.cap = "Wind directions with the fitted wrapped skew generalised normal density."}
hist(w, breaks = seq(0, 2 * pi, length.out = 37), freq = FALSE, main = "",
     xlab = "direction (radians)")
curve(dwsgn(x, fw$par["mu"], fw$par["beta"], fw$par["lam"]), 0, 2 * pi, add = TRUE, lwd = 2)
```

A goodness-of-fit look uses the probability integral transform: under a correct model,
`pwsgn()` at the fitted parameters carries the data to uniform values on $(0, 1)$. Kuiper's
statistic suits the circle because it does not depend on where the origin is placed. The
reference distribution below ignores the estimation of the parameters, so the p-value is
approximate (conservative).

```{r wind-gof, eval = has_circular, fig.cap = "Probability-probability plot of the fitted model."}
u <- sort(pwsgn(w, fw$par["mu"], fw$par["beta"], fw$par["lam"]))
n <- length(u)
plot((1:n) / (n + 1), u, xlab = "uniform plotting position", ylab = "fitted distribution function",
     pch = 20, cex = 0.6, asp = 1)
abline(0, 1, lty = 2)
V <- max((1:n) / n - u) + max(u - (0:(n - 1)) / n)
Vstar <- V * (sqrt(n) + 0.155 + 0.24 / sqrt(n))
kuiper_p <- function(v) {
  j <- 1:100
  min(1, sum(2 * (4 * j^2 * v^2 - 1) * exp(-2 * j^2 * v^2)))
}
c(V = V, V_star = Vstar, approximate_p = kuiper_p(Vstar))
```

The modified Kuiper statistic, `r if (has_circular) round(Vstar, 2) else "(not computed)"`, lies
well beyond its 1 per cent critical value of 2.001 (Stephens 1970), so the fitted law is not an
adequate description of these data in the absolute sense, although its AIC is
`r if (has_circular) round(diff((-2 * ll + 2 * npar)[c("wsgn", "wrapped_normal")]), 1) else "(not computed)"`
below that of the wrapped normal. Of the three laws compared, the smallest AIC belongs to the
`r if (has_circular) c(wsgn = "wrapped skew generalised normal", wrapped_normal = "wrapped normal", wrapped_cauchy = "wrapped Cauchy")[names(which.min(-2 * ll + 2 * npar))] else "(not computed)"`
law. The comparison is relative: a fit judged best among three may still be rejected, as here.

## Implementation

Densities, distribution functions, quantiles, the characteristic function, the trigonometric moments
and the likelihood are computed by a pure-C back-end shared with the Python package 'wsgnstat',
which compiles the same source; the two front ends return the same values for the same inputs.

## References

Agostinelli, C. (2007). Robust estimation for circular data. *Computational Statistics and Data
Analysis*, 51(12), 5867--5875.

Pewsey, A. (2000). The wrapped skew-normal distribution on the circle. *Communications in
Statistics -- Theory and Methods*, 29(11), 2459--2472.

Stephens, M. A. (1970). Use of the Kolmogorov-Smirnov, Cramér-von Mises and related statistics
without extensive tables. *Journal of the Royal Statistical Society, Series B*, 32(1), 115--122.
