---
title: "Multivariate analysis and survival"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Multivariate analysis and survival}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: ../inst/REFERENCES.bib
csl: apa.csl
link-citations: true
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = FALSE, comment = "",
                      fig.width = 7, fig.height = 4.5, dpi = 96,
                      dev.args = list(bg = "transparent"))
# Console colour carries no meaning on a rendered page. pkgdown turns it on for
# its own build, and the escape sequences then reach the reader as literal text,
# so colour is switched off here for a plain vignette render and a site build
# alike. The fixed width keeps printed output inside the documentation column.
options(cli.num_colors = 1, cli.hyperlink = FALSE, crayon.enabled = FALSE,
        width = 80)

# Figures on the package website sit on a warm off-white page in light mode and
# are inverted by pkgdown in dark mode, so an opaque background would read as a
# pale slab one way and a black plate the other. Two things paint one. The
# device canvas is made transparent by `dev.args` above, and theme_depictr()
# then inherits theme_minimal()'s white plot.background, which is drawn over
# that canvas, so it is cleared as each figure is printed. This is deliberately
# a vignette-level choice: theme_depictr() keeps its opaque background, which is
# what a figure saved for a paper wants.
transparent_bg <- ggplot2::theme(
  plot.background  = ggplot2::element_rect(fill = NA, colour = NA),
  panel.background = ggplot2::element_rect(fill = NA, colour = NA)
)
knit_print.ggplot <- function(x, ...) knitr::normal_print(x + transparent_bg)
knit_print.patchwork <- function(x, ...) knitr::normal_print(x & transparent_bg)

library(depictr)
```

Beyond regression, depictr covers three staples of applied data analysis:
principal component analysis, clustering (with quality diagnostics) and survival
curves.

## Principal component analysis

`pca_plot()` runs a PCA on the numeric columns of a data frame and draws a
biplot: the observations projected onto two components, with the variable
loadings as arrows. `scree_plot()` shows how much variance each component
explains.

```{r}
num <- c("rainfall", "fertiliser", "soil_ph", "yield")
pca_plot(crop_yield, cols = num, group = "treatment",
         title = "Crop-yield PCA")
```

```{r, fig.height = 4}
scree_plot(crop_yield, cols = num)
```

Both functions also accept a ready-made `prcomp()` object, so you can analyse
once and plot several views:

```{r}
pc <- prcomp(crop_yield[num], scale. = TRUE)
pca_plot(pc, components = c(1, 3))
```

## Clustering

`cluster_plot()` runs k-means and shows the clusters on the first two principal
components (so it works for any number of variables), with convex hulls and
labelled centroids. `dendrogram_plot()` draws a hierarchical-clustering tree and
can cut it into `k` groups.

```{r}
cluster_plot(crop_yield, cols = num, k = 3, seed = 1,
             title = "Crop-yield clusters")
```

```{r, fig.height = 4}
region_means <- aggregate(
  cbind(stress, sleep_hours, life_satisfaction, age, income) ~ region,
  data = wellbeing_survey, FUN = mean
)
rownames(region_means) <- region_means$region
dendrogram_plot(region_means[-1], k = 2, title = "Regions clustered")
```

## Choosing the number of clusters

Choosing `k` should not be guesswork. `k_diagnostic()` evaluates a
cluster-quality criterion across a range of `k` and suggests a value, using the
average silhouette width (the default, after Rousseeuw, 1987), the
within-sum-of-squares elbow or the gap statistic (Tibshirani, Walther & Hastie,
2001). See `?k_diagnostic` for the references.

```{r, fig.height = 3.5}
kd <- k_diagnostic(crop_yield, k_range = 2:6, cols = num,
                   method = "silhouette")
kd  # the diagnostic curve, with the suggested k marked
```

The suggested `k` and the underlying table are attached as attributes:

```{r}
attr(kd, "suggested")
knitr::kable(attr(kd, "k_table"), digits = 3)
```

The gap statistic compares the within-cluster dispersion against a null
reference, so unlike the other two criteria it can also support `k = 1`. Giving
it a range that starts there shows what it makes of these data. The reference
samples are drawn at random, so the chunk is seeded for a reproducible build.

```{r, fig.height = 3.5}
set.seed(1)
k_diagnostic(crop_yield, k_range = 1:6, cols = num, method = "gap")
```

The verdict is `k = 1`, which is worth taking seriously: the three-cluster
partition drawn above is a description imposed on the data rather than evidence
of groups within it.

`silhouette_plot()` then shows the quality of an actual clustering, one bar per
observation grouped by cluster, with the average silhouette width per cluster and
overall (the dashed line). Wide positive bars are well-placed observations, and a
bar below zero marks an observation that sits closer to a neighbouring cluster
than to its own.

```{r, fig.height = 5}
set.seed(1)
cl <- kmeans(scale(crop_yield[num]), centers = attr(kd, "suggested"),
             nstart = 10)$cluster
silhouette_plot(crop_yield, cl, cols = num,
                title = "Silhouette widths by cluster")
```

The overall average here is 0.24, and only six of the two hundred bars fall
below zero, none of them by much. Rousseeuw's rough guide treats an average at
or below 0.25 as no substantial structure, so this figure agrees with the gap
statistic: k-means returns clusters whether or not the data contains any, and
the silhouette is one way of asking which case you are in.

The same diagnostics apply to the wellbeing survey's numeric profile:

```{r, fig.height = 4}
wb_num <- c("age", "income", "stress", "sleep_hours",
            "exercise_days", "life_satisfaction")
wkd <- k_diagnostic(wellbeing_survey, k_range = 2:6, cols = wb_num,
                    method = "wss")
wcl <- kmeans(scale(na.omit(wellbeing_survey[wb_num])),
              centers = attr(wkd, "suggested"), nstart = 10)$cluster
silhouette_plot(na.omit(wellbeing_survey[wb_num]), wcl,
                title = sprintf("Wellbeing survey, k = %d",
                                attr(wkd, "suggested")))
```

## Survival curves

`survival_plot()` draws Kaplan-Meier curves [@kaplan1958] with stepwise
confidence limits from Greenwood's formula [@greenwood1926] and censoring marks.
The estimate is computed in base R, so no modelling package is needed. You can
pass follow-up times and an event indicator directly, a data frame or a
`survival::survfit` object.

The `clinical_trial` dataset has two arms with a real survival difference (the
treatment arm has a lower hazard), so the curves separate. Turning on the three
publication annotations gives a survminer-style figure: a number-at-risk
table beneath the curves, dashed guides to each arm's median survival and
the log-rank test of the difference as a subtitle. A survival curve is
monotone-decreasing, so its bottom-left corner is always empty, and
`legend_inside = TRUE` puts the group legend there.

```{r, fig.height = 6}
survival_plot(
  clinical_trial$time, clinical_trial$event, group = clinical_trial$arm,
  risk_table = TRUE, median_line = TRUE, logrank = TRUE, legend_inside = TRUE,
  x_lab = "Months", title = "Overall survival by arm"
)
```

The log-rank p-value is tiny and the median survival is clearly longer in the
treatment arm. Because its event times are longer, that arm is more often
right-censored at the 36-month study end, visible as the denser run of censoring
marks.

## References
