-
Notifications
You must be signed in to change notification settings - Fork 3
Backfill Annotation
Correcting or filling in individual annotation columns without re-running VEP.
Sometimes a single VariantAnnotation column is wrong or null across a whole annotation version — a data
source renamed the field VEP was reading, or a column arrived after that version was annotated. Re-running
the whole pipeline to recover one column costs weeks of VEP. Backfill annotation dumps the variants
that are already annotated, lets you annotate them however suits that one column, and UPDATEs just the
named columns back in.
Everything is driven by one management command: annotation_backfill_columns.
See issue #1675.
-
A source renamed its INFO field. The driving case
(#1673): COSMIC has called its sample count
CNT(v95/97),SAMPLE_COUNT(v99) andGENOME_SCREEN_SAMPLE_COUNT(v101), socosmic_countends up null. - A column was fixed or added and you want existing rows to carry it.
- You have a better/newer data file for one column than the version was annotated against.
This is the opposite of External Annotation at every step: it dumps variants that are annotated
rather than ones awaiting annotation, it leaves AnnotationRun / range-lock state alone entirely, it
UPDATEs named columns rather than INSERTing whole rows, and its input deliberately comes from a
different annotation source than the one that produced the version.
Run it against a version whose annotation has finished. Variants annotated after the dump take their values from the live pipeline config, so those need the pipeline fixing rather than backfilling.
dump (external) import
Annotated ─────────► VCF ──► bcftools ──► annotated VCF ─────────► UPDATE named columns
variants variant_id in INFO or VEP + INFO field COPY → temp → UPDATE
-
dump — write a VCF of the variants already annotated in a version, carrying
variant_idin INFO. -
annotate — you annotate that VCF outside VariantGrid.
bcftools annotateagainst the source VCF is the fast path; VEP works too. - import — read the annotated VCF back and update the named columns in batches.
Updates go through a temp table: per batch (default 1,000,000 records), COPY the (variant_id, col…) rows
in, ANALYZE, then
UPDATE <partition> a SET col = t.col FROM tmp t WHERE a.variant_id = t.variant_id AND a.col IS DISTINCT FROM t.col.
The UPDATE targets the version's partition table by name, so it never scans the other versions, and
IS DISTINCT FROM means postgres only rewrites rows whose value actually changes.
--columns names VariantGridColumn ids that appear in annotation.vep_columns.VEP_COLUMNS. The registry
entry supplies:
- the formatter —
cosmic_countneedsformat_pick_highest_intbecause COSMIC arrives&-joined when several records overlap a variant; choice columns (impact,sift) map to their single-char DB values. Reusing the registry formatter is what makes a backfilled value identical to a pipeline-written one. - the expected INFO field, as the default source field.
-
which columns exist for this version — a column outside the version's
columns_version/vep_versionis rejected before anything is dumped, as is a typo.
The source field is overridable per column, since the naming varies by release:
--columns cosmic_count,cosmic_legacy_id
--info cosmic_count=GENOME_SCREEN_SAMPLE_COUNT
Use --csq cosmic_count=COSMIC_SAMPLE_COUNT instead to read the value off the PICK'd CSQ entry of a
VEP-annotated file. Same registry, same formatter, different place to look.
Every named column is carried through one dump, one COPY and one UPDATE, so the row rewrite — the expensive part — happens once for the set rather than once per column.
The version defaults to the build's ACTIVE VariantAnnotationVersion; use
--variant-annotation-version <pk> to name another (e.g. a HISTORICAL one).
python3 manage.py annotation_backfill_columns --dump \
--genome-build GRCh38 \
--columns cosmic_count \
--only-missing \
--output cosmic_backfill.vcf.gz-
--only-missingrestricts to rows where every--columnsvalue is null — the common backfill shape, and much smaller than a full dump. -
--min-variant-id/--max-variant-idsplit a large dump into pieces you can annotate in parallel. - A
.gzoutput is bgzip, so it can be tabix indexed.
tabix -p vcf cosmic_backfill.vcf.gzbcftools annotate \
-a /data/annotation/VEP/annotation_data/GRCh38/Cosmic_GenomeScreensMutant_Normal_v101_GRCh38.vcf.gz \
-c INFO/GENOME_SCREEN_SAMPLE_COUNT \
-O z -o cosmic_backfill.annotated.vcf.gz \
cosmic_backfill.vcf.gzbcftools annotate matches on POS/REF/ALT and copies the INFO field across, leaving the variant_id the
dump baked in untouched. You can rename on the way through with
-c INFO/COSMIC_SAMPLE_COUNT:=INFO/GENOME_SCREEN_SAMPLE_COUNT instead of using --info at import.
python3 manage.py annotation_backfill_columns --import \
--genome-build GRCh38 \
--columns cosmic_count \
--info cosmic_count=GENOME_SCREEN_SAMPLE_COUNT \
--vcf cosmic_backfill.annotated.vcf.gz \
--dry-run--dry-run runs the COPY and counts rows against the same join predicate, then rolls back.
Same command without --dry-run. Progress (records read / rows changed) is reported per batch.
-
--batch-sizesets records per COPY/UPDATE cycle (default 1,000,000). A batch is held in memory as CSV. -
--clear-missingwrites NULL where the source is silent, making the file authoritative for the dumped variants — what a full re-annotation of that column wants. Without it (the default) a record the source says nothing about leaves the existing value alone, so a partial file fills in what it knows.
Backfilling cosmic_count on both ACTIVE versions from the COSMIC v101 VCFs already sitting in the
annotation build dirs:
for BUILD in GRCh37 GRCh38; do
COSMIC=/data/annotation/VEP/annotation_data/${BUILD}/Cosmic_GenomeScreensMutant_Normal_v101_${BUILD}.vcf.gz
python3 manage.py annotation_backfill_columns --dump --genome-build ${BUILD} \
--columns cosmic_count --only-missing --output ${BUILD}_cosmic.vcf.gz
tabix -f -p vcf ${BUILD}_cosmic.vcf.gz
bcftools annotate -a ${COSMIC} -c INFO/GENOME_SCREEN_SAMPLE_COUNT \
-O z -o ${BUILD}_cosmic.annotated.vcf.gz ${BUILD}_cosmic.vcf.gz
python3 manage.py annotation_backfill_columns --import --genome-build ${BUILD} \
--columns cosmic_count --info cosmic_count=GENOME_SCREEN_SAMPLE_COUNT \
--vcf ${BUILD}_cosmic.annotated.vcf.gz
doneOn a dev box this took ~6 minutes per build:
| version | rows |
cosmic_count before |
after |
|---|---|---|---|
| GRCh37 | 5,489,033 | 0 | 621,482 |
| GRCh38 | 8,427,888 | 57 | 747,038 |
- Compare the new count against a neighbouring column from the same source — here
cosmic_legacy_idheld 577k / 680k values from the v99 annotation, so 621k / 747k from a newer release is the right shape. A result an order of magnitude out means the annotation step matched almost nothing (usually contig naming, or an unindexed-afile). - Eyeball the top values. For COSMIC they should be the well known hotspots: BRAF V600E, KRAS G12D/G12V, IDH1 R132H, PIK3CA E545K/H1047R, TP53 R175H.
-
Contig names must match between your dump and the annotation source. VariantGrid writes contig names
(
1,2, …), so a source usingchr1needsbcftools annotate --rename-chrs. See VCF chromosome / contig names. -
Indels only match if they're represented the same way.
bcftools annotatematches POS/REF/ALT exactly, so a source that isn't left-aligned/normalised the way our variants are will miss some indels. SNVs are unaffected. -
This fixes history, not the future. Variants annotated after the dump get their values from the live
pipeline config, so a renamed source field also needs a
VEPColumnDefand avep_configupdate. - A dump of a whole version is millions of records. It streams (server-side cursor) so memory stays flat, but budget the disk: ~40MB gzipped for 5.5M variants, plus the annotated copy.
- External Annotation - the opposite direction: run VEP off-VM for variants awaiting annotation
- Variant Annotation
- Annotation Column Versions
- Variant Annotation - adding new columns
- VEP