---
title: "Bayesian SSM Analysis"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Bayesian SSM Analysis}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
set.seed(12345)
library(ggplot2)
# Output that follows the console width wraps at 77 columns, so with its "#> "
# prefix it fits in 80. Text printed with cat() does not wrap.
options(width = 77)
```

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

**Level:** Advanced. Read "Intermediate SSM Analysis" first.

::: {.alert .alert-warning role="alert"}
**Not yet peer reviewed.** The Bayesian SSM recipe this page teaches is the
package's own proposal. Its authors have not yet published it in a
peer-reviewed venue. Read it as a research tool, and state that status when
you report results from it.
:::

## 1. Overview

This vignette estimates the SSM parameters with a Bayesian model and
summarizes its posterior draws. Section 2, "Why a Bayesian SSM?", motivates
the approach. Section 3, "The cosine model as a linear regression", rewrites
the cosine model so that a regression can fit it. Section 4, "Fitting the
model with brms", fits it to the `jz2017` octant scores. Section 5, "From
posterior draws to SSM summaries", turns the draws into SSM summaries with
`ssm_draws()`. Section 6, "The induced prior on amplitude", shows what the
priors on $x$ and $y$ imply for the amplitude. The Wrap-up names related
functions and the next page to read.

## 2. Why a Bayesian SSM?

The Structural Summary Method (SSM) describes a circumplex profile with an
elevation $e$, an amplitude $a$, and a displacement $d$.
`vignette("introduction-to-ssm-analysis")` defines these three parameters.
The package's `ssm_analyze()` estimates them with bootstrap or Monte Carlo
confidence intervals.

A Bayesian alternative is attractive when you want prior information or
hierarchical structure. Prior information is what you know about the
parameters before you see the data. An example of hierarchical structure is partial
pooling across persons or groups, where each person's or group's estimate is
pulled toward the overall estimate. A Bayesian alternative is also attractive
when you want full posterior distributions for derived quantities, such as
amplitude and displacement. A posterior distribution combines the prior and
the data into one distribution over the parameters.

The division of labor is deliberate. A general-purpose Bayesian package, such
as **brms**, fits the model. Then `ssm_draws()` converts the resulting
posterior draws into SSM parameter draws. Posterior draws are samples
from the posterior distribution. `ssm_draws()` summarizes them with the same
circular statistics (statistics for angles) that the rest of the package uses. Displacement is an
angle, so its posterior needs circular treatment. In particular, an interval
that straddles the 0°/360° boundary must wrap across 0° rather than invert.
An inverted interval runs the long way around the circle. `ssm_draws()`
handles that by construction.

## 3. The cosine model as a linear regression

The SSM's cosine model for a profile of scores $S_j$ observed at scale
angles $\theta_j$ is

$$S_j = e + a \cos(\theta_j - d).$$

Expanding the cosine of a difference gives

$$S_j = e + \underbrace{a \cos d}_{x} \cos \theta_j +
\underbrace{a \sin d}_{y} \sin \theta_j,$$

a linear regression of the scores on $\cos \theta_j$ and $\sin \theta_j$.
The intercept is the elevation $e$, the *cosine* coefficient is $x$, and the
*sine* coefficient is $y$. The amplitude $a$ and displacement $d$ are
recovered by

$$a = \sqrt{x^2 + y^2}, \qquad d = \operatorname{atan2}(y,\, x),$$

with the displacement wrapped into $[0°, 360°)$.

Note the argument order: `atan2(y, x)` takes the **sine coefficient
first**. Swapping the arguments is a classic silent error. It returns a
valid-looking angle that is wrong for almost every profile. The example below
pins the convention with a profile whose displacement is known to be 90°.
The swapped call would return 0° instead:

```{r known-direction}
# Truth: e = 1, a = 2, d = 90 degrees
theta <- as.numeric(octants()) * pi / 180
scores <- 1 + 2 * cos(theta - pi / 2)
fit <- lm(scores ~ cos(theta) + sin(theta))
x_hat <- coef(fit)[["cos(theta)"]]
y_hat <- coef(fit)[["sin(theta)"]]
d_hat <- atan2(y_hat, x_hat) * 180 / pi
round(c(x = x_hat, y = y_hat, d = d_hat), 6)
```

The printed `d` is 90. A hidden check stops the vignette build unless
`atan2(y_hat, x_hat)` gives 90° and the swapped `atan2(x_hat, y_hat)` does not.

```{r known-direction-check, echo = FALSE}
stopifnot(isTRUE(all.equal(d_hat, 90)))          # atan2(y, x): correct
stopifnot(!isTRUE(all.equal(atan2(x_hat, y_hat) * 180 / pi, 90)))  # swapped
```

## 4. Fitting the model with brms

We model raw octant scores from the `jz2017` data. Octant scores are scores
on eight scales placed 45° apart around the circle. The data are in long
format, with one row per person-scale observation. A random intercept per
person gives each person their own baseline level. It is included to absorb
the dependence among a person's eight scores. The fixed effects are the
coefficients shared by all persons (intercept, cosine coefficient, sine
coefficient). They are then the group-level $(e, x, y)$. A random subsample
of 200 persons, drawn with a fixed seed, keeps the example light.

The long data frame is `dat`, with 1600 rows: one per person and octant scale.
Its columns are the person `id`, `cos_theta` and `sin_theta` (the cosine and
sine of the scale's angle from `octants()`), and the `score`. (The code that
draws the subsample and builds `dat` is omitted. It builds the same data as
the seeded script named below.) Its first rows are:

```{r data-prep, echo = FALSE}
data("jz2017")
set.seed(12345)
sub <- jz2017[sample(nrow(jz2017), 200), ]
scales <- c("PA", "BC", "DE", "FG", "HI", "JK", "LM", "NO")
theta <- as.numeric(octants()) * pi / 180
dat <- data.frame(
  id = rep(seq_len(200), times = length(scales)),
  cos_theta = rep(cos(theta), each = 200),
  sin_theta = rep(sin(theta), each = 200),
  score = unlist(sub[scales], use.names = FALSE)
)
```

```{r data-head}
head(dat)
```

**brms** fits its models with Stan, and Bayesian sampling with Stan requires a
working Stan toolchain. So the model below is
not re-fitted when this vignette is rebuilt. Its posterior draws were
generated once by the seeded script `data-raw/bayesian_ssm_draws.R`, and
they ship with the package. The `normal(0, 1)` prior on the regression
coefficients is a deliberate modeling choice. Section 6 examines its
consequences for the amplitude.

```{r brms-fit, eval = FALSE}
library(brms)
bfit <- brm(
  score ~ cos_theta + sin_theta + (1 | id),
  data = dat,
  prior = set_prior("normal(0, 1)", class = "b"),
  chains = 4, iter = 2000, cores = 4, seed = 12345
)
draws <- as.matrix(bfit,
                   variable = c("b_Intercept", "b_cos_theta", "b_sin_theta"))
```

## 5. From posterior draws to SSM summaries

The draws form a matrix with one row per posterior draw and three columns
interpreted **in column order** as $(e, x, y)$. The vignette reads the shipped
draws into the matrix `draws`. (The code that reads the file is omitted.)

```{r load-draws, echo = FALSE}
draws <- readRDS("bayesian_ssm_draws.rds")
```

```{r draws-shape}
dim(draws)
head(round(draws, 3))
```

`ssm_draws()` accepts two draw shapes. The first is three-column
*parameter* draws like these. The second is *profile* draws: posterior draws
of a whole profile of scale scores, with one column per scale and `angles`
supplied. A three-column matrix without angles is
ambiguous, because it could also be profile draws from a three-scale
instrument. So the shape must be stated explicitly with
`type = "parameters"`:

```{r adapter}
res <- ssm_draws(draws, type = "parameters")
summary(res)
```

Each posterior draw of $(x, y)$ was converted to a draw of $(a, d)$. Because
$d$ is an angle, the displacement's credible interval comes from circular
quantiles. A credible interval is the Bayesian counterpart of a confidence
interval. The circular mean is the direction of the average of the angles
taken as unit vectors. The draws are centered on their circular mean and
unwrapped (laid on a line with no jump near the mean). Then the quantiles are
taken, and the bounds are re-wrapped. The interval never comes from naive
linear quantiles that would misbehave near 0°/360°.

Point estimates are posterior medians for the linear parameters: elevation,
$x$, $y$, amplitude, and fit where it is available. The amplitude posterior is right-skewed, so a mean
would overstate it. For displacement, the point estimate is the circular
mean.

One caveat to keep in mind: these marginal summaries are not jointly
coherent. A marginal summary describes one parameter's draws on its own, so
the summaries need not agree with each other. The reported amplitude is the median of the amplitude draws. It is
not $\sqrt{x^2 + y^2}$ evaluated at the reported $x$ and $y$. Likewise, the
reported displacement is not the direction of the reported $(x, y)$. Each is
the honest marginal summary of its own posterior.

Model fit ($R^2$) is not reported here, because parameter draws carry no
profile to measure fit against. Profile draws (the second shape) do yield
fit draws from `ssm_draws()`.

## 6. The induced prior on amplitude

Independent priors on $x$ and $y$ do not induce a flat prior on the
structural parameters. The induced prior is the prior on $a$ and $d$ that
the priors on $x$ and $y$ imply. With $x, y \sim \mathrm{Normal}(0, 1)$, the implied
prior on $a = \sqrt{x^2 + y^2}$ is Rayleigh-shaped: zero density at $a = 0$,
then a single peak, with a right skew. Its mass is pushed *away* from $a = 0$.
But the implied prior on $d$ is uniform. A prior-predictive simulation makes
the amplitude prior visible. It draws 10000 values each of $x$ and $y$ from
$\mathrm{Normal}(0, 1)$ and computes $a = \sqrt{x^2 + y^2}$ for each pair. The
histogram shows those draws of $a$. The two printed numbers are their median
and the share of them below 0.1. (The simulation and plotting code is omitted.)

```{r induced-prior, echo = FALSE, fig.width = 5, fig.height = 3}
x_prior <- rnorm(10000, 0, 1)
y_prior <- rnorm(10000, 0, 1)
a_prior <- sqrt(x_prior^2 + y_prior^2)
ggplot(data.frame(a = a_prior), aes(x = a)) +
  geom_histogram(bins = 60, fill = "grey35") +
  labs(
    x = "Amplitude implied by the priors on x and y",
    y = "Prior draws",
    title = "Rayleigh-shaped induced prior on amplitude"
  ) +
  theme_minimal()
round(c(prior_median = median(a_prior), prior_mass_below_0.1 =
          mean(a_prior < 0.1)), 3)
```

The Rayleigh shape is not a defect. It is a modeling choice to be aware of:
the prior mildly disfavors exactly-flat profiles ($a = 0$). If your application needs prior
mass concentrated near $a = 0$, place priors on $(a, d)$ directly in a
custom Stan model instead. Its posterior draws can still be summarized
here. Convert them to $(x, y) = (a \cos d, a \sin d)$, or pass profile
draws.

## Wrap-up

* Per-person descriptive SSM parameters (no pooling):
  `ssm_parameters_id()` and its `summary()` method.
* Hierarchical pooling across persons or groups is exactly what the
  brms/Stan route is for. Fit the model you need, and feed the posterior
  draws (parameter or profile shape) back through `ssm_draws()`.
* Frequentist inference on group profiles and contrasts: `ssm_analyze()`.

The next page to read is "Growth Models on SSM Parameters". It fits a growth
model to SSM parameters measured at several waves and turns its fixed
effects into amplitude and displacement curves with `ssm_draws()`.
