PolyGenius
Contents

compute$scores

Compute polygenic scores

Computes PRS values for every model in a PolyGeniusStudy by driving PLINK2 --score over the study's genotypes. Returns the score matrix and writes nothing into study; the caller assigns the result into a data$scores layer.

Usage

compute$scores(
  study,
  maf.thr = 0,
  model.filter = function(variants) rep(TRUE, nrow(variants)),
  models = NULL,
  simplify = FALSE,
  memory = NULL,
  nthreads = NULL
)

Arguments

ArgumentDescription
studyA study whose own model library is empty is fine as long as models supplies them; the call aborts only when neither does.
maf.thrNumeric scalar in [0, 0.5], default 0. Variants whose in-sample frequency falls outside [threshold, 1 - threshold] are excluded and reported as maf.filtered; any value above 0 forfeits the fused scoring path.
model.filterFunction of one argument, default function(variants) rep(TRUE, nrow(variants)). Called once per model with that model's variant table and must return a logical vector of length nrow(variants) with no NA; anything else aborts, naming the offending model. A supplied filter also disables the file-backed regime for the call, and a non-identity filter that removes nothing across the whole library logs a warning. Check the chromosome spelling before comparing: a model's chr column may be "19" or "chr19", so v$chr == 19 can silently match nothing and leave a negated filter retaining every row.
modelsA PGS, a PGSLibrary, a variant data.frame, a named list of variant data.frames, a list of PGS objects, or omitted (the default). Explicit models to score instead of the ones attached to study; omitting it scores every attached model. Supplying it as NULL aborts, because that is an expression that resolved to nothing rather than a request to score everything. Each variant table needs chr, position, ea, nea and beta columns, and a raw table additionally requires study's genotype to expose a genome build. Any other shape aborts. Score columns are named after the models. For a list, the list's own names win over each PGS's $name, and an entry left unnamed keeps its model's; a list of variant data.frames has no other name source, so an unnamed entry there aborts. Two models may share a name, and the returned matrix then carries that label twice. A name labels a column; position identifies it. Subset such a layer by column number, not by name -- scores[, "shared"] returns the first match twice.
simplifyLogical scalar, default FALSE. When TRUE and exactly one model is scored, the result is dropped to a named numeric vector instead of a one-column matrix.
memoryPositive numeric scalar (GiB), a named list, or NULL (default). Memory budget; NULL resolves from workspace$config$max.memory, and a bare scalar is a total GiB budget of its own. The list form overrides any subset of total (GiB), plink (MB), cells (integer cell count) and file.backed (integer cell count); any other key aborts. See Memory and batching.
nthreadsPositive integer scalar, or NULL (default). Passed to PLINK --threads; NULL omits the flag and leaves PLINK's own default.

Value

A numeric matrix of PRS values (samples x models), columns in the requested model order, or — with simplify = TRUE and exactly one model — a named numeric vector. Either carries a provenance record (provenance()) whose misc holds strategy ("fused" or "via.extract"), n.batches, n.models, n.variants.total and maf.thr, plus two diagnostics entries, coverage (diagnostics(x, "coverage")) and the variant fate record (diagnostics(x, "fate")), each described in its section above.

Details

The variants of every scored model are collated into one union and matched to the genotype records (PLINK .pvar/.bim) by chromosome and position, accepting a record when the alleles agree in either orientation (nea == ref & ea == alt, or nea == alt & ea == ref). Each matched record takes the canonical id chr:pos:ref:alt, the same id PLINK assigns via --set-all-var-ids, so the score file needs no rename step. Orientation is resolved by PLINK and never in R: the effect allele goes into the score file's EA column and --score counts that allele's dosage directly, so no dosage is ever flipped.

Genotypes split across several files are matched and scored per file and the per-file PRS values summed, which is exact because a PRS is additive. Files are scored sequentially; nthreads parallelises only within a single PLINK call.

A model variant matching more than one genotype record is excluded and reported as collision. Multi-allelic records (--max-alleles 2) and duplicated chr:pos:ref:alt records (--rm-dup exclude-all) are dropped inside PLINK and reported as missing.

A model that lists a variant more than once -- one loaded from an older library or .pgd, or built by a route that bypasses PGS() -- is scored under the rule PGS() applies: exact copies count once, and a variant listed with conflicting betas is left out. Either case warns, naming the models.

Memory and batching

memory resolves into three knobs at three layers: cells, this call's per-batch cap in cells (V_batch x M_batch), not bytes; plink, PLINK's own --memory flag in MB, a genuine concurrent reservation; and file.backed, the dense backbone$n.variants() x n.models cell count above which the scored union is read out-of-core from the model-weight store (backbone-backed input only). memory = NULL falls back to workspace$config$max.memory, whose own default Inf means no budget at all: cells = 200e6, PLINK uncapped, file.backed = 2.5e8. Under a total budget, plink is subtracted from total before cells is derived from the remainder, file.backed is derived from the full un-subtracted total, and any knob supplied explicitly wins verbatim over its derivation. cells is the master knob for peak memory: all models score in one fused genotype pass while the scored union times the model count fits it, and switch to a batched via-extract path once it does not. maf.thr > 0 also forfeits the fused path but adds no extra genotype read, since the frequency computation is folded into the same canonical-extraction pass.

Variant fate record

diagnostics(x, "fate") is a PolyGeniusVariantFate, holding what became of every requested variant: counts (named integers over scored, missing, maf.filtered, no.freq, collision, counted over unique variants), n.requested, n.flipped, n.strand.ambiguous, and counts.by.genotype when more than one genotype contributed.

Printing it shows those counts and a short head; as.data.table() and as.data.frame() expand it into one row per requested variant per genotype — chr, position, nea, ea, status, matched, flipped, strand.ambiguous, freq, n.records. It stores a byte per variant per genotype rather than that table, so the detail is always kept and there is no retention mode to choose. It sits beside coverage in diagnostics() rather than in provenance, whose misc must stay bounded.

provenance(x)$params records every supplied argument as the expression the caller wrote, so a model library or a filter closure is named rather than retained inside the study's serialized form. What the request resolved to is in misc: n.models and n.variants.total, both counted after model.filter, and the applied maf.thr. n.variants.total is the sum of per-model variant counts, so it exceeds the number of distinct variants whenever two models share one.

Coverage diagnostics

diagnostics(x, "coverage") is a data.table, one row per model in column order, with columns model, n.weights, n.scored, weight.total, weight.scored, coverage.var, coverage.count and coverage.palindromic. coverage.var is sum(beta^2 * 2p(1-p)) over scored variants divided by the same sum over every requested variant, with p the model's own source-plane eaf; it is NA for a model whose variant table carries no usable eaf, and coverage.count (n.scored / n.weights) is the always-computable naive fallback. A variant a model lists more than once counts once in n.weights; one listed with conflicting betas is never scored, so it counts in n.weights but not in n.scored, and adds nothing to the weight sums. coverage.palindromic is the strand-ambiguous share of sum(beta^2) over every requested variant, which coverage.var cannot supply: a model scored perfectly backwards still reads coverage.var = 1.0. The entry stays on this layer only — assigning the layer into data$scores copies it nowhere else; see diagnostics().

Examples

data$scores$X <- compute$scores(data)

# Cap peak memory at 8 GiB.
data$scores$X <- compute$scores(data, memory = 8)

# Score an ad hoc model library, dropping near-zero weights.
scores <- compute$scores(data, models = my.models,
                         model.filter = function(v) abs(v$beta) > 1e-4)
coverage <- diagnostics(scores, "coverage")

See Also

compute$scores.plan() for an a-priori resource estimate (predicted peak RAM, batch count, strategy, and what a given memory value resolves to); it never runs PLINK and never returns a score matrix.

Aliases: compute-scores, compute.scores, compute$scores