---
title: "Predicting Weighted Sum Scores from plasma biomarkers"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Predicting Weighted Sum Scores from plasma biomarkers}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

# Keep data.table within CRAN's thread limits
data.table::setDTthreads(2)

# RNOmni, glmnet and joinet are suggested packages; chunks that need them are
# skipped without them
has_rnomni <- requireNamespace("RNOmni", quietly = TRUE)
has_model <- requireNamespace("glmnet", quietly = TRUE) &&
  requireNamespace("joinet", quietly = TRUE)
```

```{r load}
library(WSSmod)
```

## Why predict Weighted Sum Scores

Measuring the full proteomic panel is expensive. `predict_WSS()` instead
predicts all 75 module scores of the bundled `"core_AD_plasma_biomarkers"`
reference set from age, sex and eight plasma biomarkers, using a fitted
`joinet` model (a two-layer elastic net) shipped with the package. The
vignette [Calculating Weighted Sum Scores](calculating-wss.html) describes the
scores themselves; the modules, biomarkers and prediction model are
described in the [preprint](https://www.medrxiv.org/content/10.64898/2026.08.14.26360394v1.full-text).

The prediction has two steps:

1. `normalize_wss_biomarkers()` puts the raw biomarker values on the
   rank-based inverse normal transformed (INT) scale the model was trained on.
2. `predict_WSS()` returns the module scores.

`predict_WSS()` needs the suggested packages `glmnet` and `joinet`. Chunks
below that use them run only if both are installed.

## The inputs

The model uses ten columns:

* `Age`, in years,
* `Gender`, numeric, with `1` for male and `0` for not male,
* eight raw plasma biomarkers: `PlasmaPTau181`, `PlasmaAB142P`,
  `PlasmaAB140P`, `PlasmaABRatio`, `PlasmapTau217`,
  `PlasmapTau217_AB42Ratio`, `PlasmaGFAP` and `PlasmaNfL`.

Plasma pTau181, pTau217, Aβ42, Aβ40, NfL and GFAP were measured on the fully
automated Lumipulse G 1200 platform (Fujirebio US, Inc, Malvern, PA),
following the recommendations of the NACC Working Group. The two ratio columns
are derived from these measures (Aβ42/Aβ40 and pTau217/Aβ42).

### Example data

The package ships `wss_example_biomarkers`, simulated values for 20 patients
with these columns. They have no biological meaning.

```{r inputs}
dim(wss_example_biomarkers)
head(wss_example_biomarkers, 3)
```

Only the biomarkers are normalized. `Age` stays in raw years and `Gender`
stays 0/1.

```{r split}
# The eight raw biomarker columns, without Age and Gender
biomarker_cols <- setdiff(names(wss_example_biomarkers), c("Age", "Gender"))
raw_biomarkers <- wss_example_biomarkers[, biomarker_cols]
```

## Choosing a normalization

`normalize_wss_biomarkers()` has two methods. Both return a data frame with
the same column names and row names as the input.

### `method = "project"` (default)

Each raw value is placed within a bundled reference distribution of baseline
values with `project_rank_norm()`, one biomarker at a time. A value does not
depend on the other patients in your data, so this works for a single patient
as well as for a cohort.

```{r project-one}
# One patient
one_patient <- normalize_wss_biomarkers(raw_biomarkers[1, ])
round(one_patient, 2)
```

The same patient gets the same normalized values when scored with others:

```{r project-cohort}
# A cohort of 20 patients
projected <- normalize_wss_biomarkers(raw_biomarkers)
all.equal(projected[1, ], one_patient)
```

The bundled reference is anonymized: each biomarker's values were shuffled
independently, so it preserves the distribution of each biomarker but not the
relationships between them. `load_prebuilt_biomarker_reference()` returns it.

### `method = "self"`

Each biomarker is rank-normalized within your own data with
`RNOmni::RankNorm()`. The result depends on who else is in the data, and it
needs at least two patients, so it is appropriate for a sizeable cohort whose
distribution you are comfortable normalizing against directly.

```{r self, eval = has_rnomni}
self_normalized <- normalize_wss_biomarkers(raw_biomarkers, method = "self")
round(head(self_normalized, 3), 2)
```

With a single patient it stops with an error:

```{r self-one, eval = has_rnomni}
tryCatch(
  normalize_wss_biomarkers(raw_biomarkers[1, ], method = "self"),
  error = function(e) conditionMessage(e)
)
```

### Which to use

* For one patient or a small group, use `"project"`.
* For a larger cohort, either works. `"project"` keeps the values comparable
  with other data scored the same way and with the model's training scale.
  `"self"` instead centers each biomarker on your own cohort, which suits
  an analysis of that cohort alone.
* Do not mix the two in one analysis.

## Predicting module scores

Add `Age` and `Gender` back to the normalized biomarkers and call
`predict_WSS()`.

```{r predict, eval = has_model}
newdata <- cbind(
  wss_example_biomarkers[, c("Age", "Gender")],
  projected
)
pred <- predict_WSS(newdata)
names(pred)
```

The result is a list of two matrices with one row per patient, named as in
`newdata`, and one column per module:

* `meta` holds the final predictions, from the second layer of the model. These
  are the scores to use.
* `base` holds the predictions of the first layer alone, before the modules'
  correlations are used to refine them. They are returned for comparison.

```{r predict-shape, eval = has_model}
dim(pred$meta)
round(pred$meta[1:3, 1:4], 2)
```

The columns are the same modules that `calculate_WSS()` scores with the default
reference set, in the order of the model's outcomes. A patient scored
alone matches the same patient scored in a cohort:

```{r predict-one, eval = has_model}
single <- predict_WSS(cbind(wss_example_biomarkers[1, c("Age", "Gender")],
                            one_patient))
all.equal(single$meta[1, ], pred$meta[1, ])
```

### Reading the scores

* A predicted score is on the INT-based scale of the bundled reference set,
  centered near zero. A higher value means more activity of the module in the
  direction associated with the core biomarkers, as for measured scores.
* Predicted scores are less spread out than measured ones, because a model
  with few inputs cannot reproduce all of the variation:

```{r spread, eval = has_model}
# Standard deviation of the predicted scores, first five modules
round(apply(pred$meta, 2, sd)[1:5], 2)
```

* Compare predicted scores with other predicted scores. They are not directly
  comparable with scores computed by `calculate_WSS()` from data on a different
  scale, such as z-scaled log2 ratios.
* Human-readable labels for the modules are in
  `load_prebuilt_terms("core_AD_plasma_biomarkers")`.

```{r boxplot, eval = has_model, fig.width = 6, fig.height = 3.5, fig.alt = "Horizontal boxplots of predicted WSS scores for the first eight modules, labeled by module term, across 20 simulated patients."}
# Spread of the first eight predicted module scores (simulated data, no
# biological meaning), labeled by module term
terms <- load_prebuilt_terms("core_AD_plasma_biomarkers")
labels <- terms$term[match(colnames(pred$meta)[1:8], terms$name)]
par(mar = c(4, 14, 1, 1))
boxplot(pred$meta[, 1:8], names = labels, horizontal = TRUE, las = 1,
        cex.axis = 0.7, xlab = "Predicted WSS score")
```

## Intended use and limits

* `predict_WSS()` is for research use only. It is not a diagnostic tool.
* The model was trained on a single baseline cohort. Its performance in other
  cohorts, assays or populations has not been established.
* The biomarker inputs must be measured as in the training data (see "How the
  biomarkers were measured"). Values from a different assay or platform can
  produce meaningless predictions even after normalization.
* All ten inputs are required, and missing values are not supported.
* The data in this vignette are simulated and have no biological meaning.
