Skip to content

API reference: c4.survival

The code behind c4 survival and c4 prs. For command-line use, see Command line. Covariates, endpoints and VCFs come from the cohort's survival: block, described in the data catalog.

from c4.survival import Survival

surv = Survival.from_cohort("demo")
results = surv.run_survival(["4:1455432:A>T"], build="b38", endpoint="OS")

How the model works

Who is in the model. The tables named in the survival: block are merged on patient_id, and the first table defines the study population. Each patient is linked to their VCF sample through the id_map (for dna: imputed, the imputed_id column). Patients marked drop=1 are left out.

Per variant (run_survival). Dosages are extracted with bcftools. If the variants are in another build than the VCFs, they are lifted over first. Dosage counts the ALT allele as you wrote it; if the VCF has REF and ALT the other way round, the dosage is flipped to 2 - dosage and the variant gets a warning. Each variant then gets its own Cox proportional hazards model (lifelines CoxPHFitter):

time, event ~ dosage + covariates
  • The model is fitted on complete cases only: rows missing the time, event, dosage or any covariate are dropped. So N can differ between variants.
  • Covariates whose names start with PC are standardised to mean 0, SD 1 on that sample. Other covariates are used as they are.
  • HR is the hazard ratio per extra copy of the ALT allele. ALT_AF is that allele's frequency in the sample; MAF is the smaller of ALT_AF and 1 - ALT_AF.
  • A variant that can't be tested still gets a row: its HR is blank and error says why (not in the VCF, liftover failed, no complete cases, model didn't converge).

Polygenic score (run_prs). Each weight's effect allele is matched to the variant's REF or ALT. Weights matching neither are dropped, and so are palindromic A/T and C/G SNPs unless keep_ambiguous=True. The score is the sum of effect × effect-allele dosage; where a person's dosage is missing, that variant's mean is used instead. The score is standardised on the complete-case sample and goes into the same Cox model in place of the dosage, so HR_per_SD is per standard deviation of the score. With n_quantiles, a second model compares the top quantile against the bottom one.

Reference

COVARIATES = [*[f'PC{i}' for i in (range(1, 6))], 'female', 'Age_IOstart'] module-attribute

DEFAULT_ENDPOINTS = {'OS': ('OS_mo', 'OS_event'), 'PFS': ('PFS_mo', 'PFS_event')} module-attribute

Survival

from_cohort(name, catalog=None) classmethod

Build from a cohort's survival: block in the data catalog.

The tables listed under tables are loaded and merged on patient_id, and each patient's sample name in the dna VCFs is looked up in the cohort's id_map. Covariates and endpoints come from the block, and liftover chains from the catalog's resources.

chain_for(from_build)

Chain file for lifting variants from from_build to the VCF build, if one is known.

prep_survival(variant_list, build, dosage)

Parse variants, liftover if needed, extract dosages, align alleles.

Returns:

Type Description
DataFrame

A tuple (dosage_df, variant_meta). dosage_df has the sample ID

dict

column plus one column per variant string, counting the ALT allele

tuple[DataFrame, dict]

as written. variant_meta maps each variant string to

tuple[DataFrame, dict]

{"vcf_id", "warning", "error"}.

last_dosage_table()

(dosages, info) from the last run_survival/run_prs, for saving: one row per VCF sample (with patient_id when known) and one column per variant, plus the per-variant match table. None before any run.

run_survival(variant_list, build, endpoint, dosage=True, keep_data=False)

Fit one Cox PH model per variant: time/event ~ dosage + covariates.

Each model uses the complete cases for that variant, endpoint and the covariates; PC covariates are standardised on that sample. The HR is per copy of the ALT allele as written in the variant string.

Parameters:

Name Type Description Default
variant_list list

Variant strings, e.g. "4:1455432:A>T" or "4:1455432:A:T".

required
build str

Genome build of the variants (e.g. "b37", "b38"). Lifted to the VCF build when they differ.

required
endpoint str

Endpoint name, e.g. "OS" or "PFS".

required
dosage bool

Use the VCF's DS field; False derives dosage from GT.

True
keep_data bool

Also return, per variant, the complete-case DataFrame (time, event, dosage) its model was fitted on, e.g. for a Kaplan-Meier plot.

False

Returns:

Type Description

DataFrame with one row per variant: variant, vcf_id, HR,

HR_lower_95, HR_upper_95, p, MAF, ALT_AF, N, N_events, warning,

error. With keep_data, a tuple (results, {variant: data}).

run_prs(weights, build, endpoint, dosage=True, keep_ambiguous=False, n_quantiles=0)

Build a weighted polygenic risk score (PRS) and test its association with a survival endpoint via Cox PH.

The score is the sum of effect x effect-allele dosage over matched variants; a missing dosage is filled with that variant's mean. The score is standardised on the complete-case sample, so the HR is per SD.

Parameters:

Name Type Description Default
weights DataFrame

Columns variant_id (chr:bp:ref:alt or chr:bp:ref>alt), effect_allele, effect (additive beta), as read by read_prs_weights.

required
build str

Genome build of the weight variants (e.g. "b37", "b38").

required
endpoint str

Endpoint name, e.g. "OS" or "PFS".

required
dosage bool

Use the VCF's DS field; False derives dosage from GT.

True
keep_ambiguous bool

Keep palindromic (A/T, C/G) SNPs instead of dropping them.

False
n_quantiles int

If > 1, also fit top vs bottom quantile.

0

Returns:

Type Description
DataFrame

A tuple (per_variant, summary). per_variant has one row per

dict

weight with whether it was used and, if not, why (note).

tuple[DataFrame, dict]

summary is a dict with HR_per_SD, HR_lower_95, HR_upper_95, p, N,

tuple[DataFrame, dict]

N_events, variant tallies (n_matched, n_not_in_vcf, ...),

tuple[DataFrame, dict]

per_sample (each person's raw and standardised score) and,

tuple[DataFrame, dict]

with quantiles, quantile_HR. On failure it has error.

read_prs_weights(path)

Read a PRS weights file: tab-separated, no header, exactly three columns (variant_id, effect_allele, effect).

Raises ValueError, naming the offending line, if a line has a different number of columns, a value is missing, or an effect isn't a number.