Skip to content

fix: multi-allelic statistics and capture filtering in variant-info - #40

Merged
dlopez-bioinfo merged 5 commits into
masterfrom
fix/multiallelic-and-capture
Sep 30, 2026
Merged

dlopez-bioinfo merged 5 commits into
masterfrom
fix/multiallelic-and-capture

Conversation

@dlopez-bioinfo

@dlopez-bioinfo dlopez-bioinfo commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Three independent problems around multi-allelic sites and capture regions, reported on the CSVS hs37d5 database.

1. variant-info listed carriers outside their capture BED

variant_info() intersected the per-variant bitmaps with the sample filter only, so a sample with a call outside its own BED was listed as a carrier while query() excluded it from AC, AN and the genotype tallies. It now uses the same capture- and ploidy-aware eligible set as query().

On afquery_db_hs37d5 this accounts for every mismatch reported:

locus before (variant-info) after query
1:6508625 A>G 120 het / 8 hom / 7 alt 95 / 7 / 0 N_HET=95, N_HOM_ALT=7, N_FAIL=0
13:44995309 G>A 218 het / 77 hom / 4 alt 218 / 73 / 1 218, 73, 1
13:44995309 G>T 35 het / 2 hom 34 / 2 34, 2

Behaviour change: the guide previously described the extra off-target carriers as expected; it is rewritten, and the debugging table no longer relies on that difference.

2. N_HOM_REF counted carriers of other alleles at multi-allelic sites

N_HOM_REF was the residual n_eligible - N_HET - N_HOM_ALT - N_FAIL - N_NO_COVERAGE. The eligible set depends only on the position, so at a site with several ALT alleles every carrier of another allele was counted as hom-ref for this one. The tallies for an allele now leave those samples out.

  • Invariant change: at a biallelic site the five categories still add up to n_eligible. At a multi-allelic site they fall short by exactly the number of eligible samples carrying only another allele. AC, AN and AF are not affected. Documentation updated.
  • Coverage evidence per position: --min-pass, --min-observed and --min-quality-evidence now count calls for any allele at the position, and a sample with such a call is never reported as N_NO_COVERAGE.
  • The per-allele computation, previously repeated in query, region, batch, dump and annotate, now lives in QueryEngine._variant_stats. annotate also excludes carriers of stored alleles when the requested allele is absent from the database.
  • The test oracle had the same residual and is corrected.
  • Known limitation: the build-time --min-covered gate (filtered_bitmap) is still evaluated per allele. Carriers of other alleles are removed from it at query time, but at a multi-allelic site a rare allele's row can still report true hom-ref samples as N_NO_COVERAGE while the common allele's row counts them as hom-ref. Making it per position requires rebuilding the database.

At 13:44995309 N_HOM_REF goes from 165 (G>A) / 421 (G>T) to 141 for both, the number of G/G samples: 457 eligible minus the 316 carrying A, T or both (12 samples carry both). The commit message for this change says 36 samples were misclassified for G>A; the correct figure is 24, because the 12 samples carrying both alleles were already counted as carriers of A.

3. normalize_vcf.sh did not split multi-allelic records

bcftools norm ran without -m, and with -d passed twice; the effective -d both treats two different indels at one position as duplicates. Now -m -both -d exact. bcftools refuses that combination before 1.20 (checked with 1.18 and 1.19), so the script checks the version. VCFs normalized with the old script need re-normalizing and the database rebuilding; documented in the preprocessing guide.

Closes #39

Validation

  • Full suite passes (618 tests). The regression tests for each fix fail on master.
  • New tests/test_multiallelic.py: point, region, batch (one allele requested), batch-multi, dump, annotate, variant-info and --min-pass at a multi-allelic site. New oracle cohort with a 1/2 carrier, partial capture and a FILTER failure.
  • Read-only comparison of master against this branch on afquery_db_hs37d5, whole chr22 (585,928 rows, 116,802 at multi-allelic positions):
    • default filters: no biallelic row changes; only N_HOM_REF changes, on 114,232 multi-allelic rows; the invariant holds on every row;
    • --min-pass 1: no biallelic row changes; AC/AN unchanged; N_NO_COVERAGE drops on 2,292 multi-allelic rows.
  • Full-chromosome region query: 15.8 s on master, 18.1 s here (allele pooling at multi-allelic positions).
  • normalize_vcf.sh run with bcftools 1.20 and 1.24 on the 2:136592357 record: two biallelic records, CAAAAAAA>C and C>CAAAAA, none dropped. Clear error with 1.18 and 1.19.
  • mkdocs build --strict passes.

No database rebuild is needed for fixes 1 and 2; they apply at query time.

variant-info intersected the per-variant bitmaps with the sample filter
only, so a sample with a call outside its own capture BED was listed as
a carrier while query() excluded it from AC, AN and the genotype
tallies. Use the same capture- and ploidy-aware eligible set as query(),
so both commands agree.

On the CSVS hs37d5 database this accounts for every reported mismatch,
e.g. at 1:6508625 variant-info listed 120 het / 8 hom / 7 alt against
N_HET=95 / N_HOM_ALT=7 / N_FAIL=0 from query.
…elic sites

N_HOM_REF was the residual n_eligible - N_HET - N_HOM_ALT - N_FAIL -
N_NO_COVERAGE. The eligible set depends only on the position, so at a
site with several ALT alleles every sample carrying another allele was
counted as hom-ref for this one. At chr13:44995309 G>A,T this reported
N_HOM_REF=165 for G>A while 36 of those samples carry G>T.

The tallies for one allele now leave out eligible samples that carry
only another allele at the position. At a multi-allelic site the five
categories therefore add up to less than n_eligible, by exactly that
number of samples; biallelic sites are unchanged. AC, AN and AF are not
affected. The documented invariant is updated accordingly.

Coverage evidence (--min-pass, --min-observed, --min-quality-evidence)
is now judged per position: a call for any allele shows the position
was sequenced, and such a sample is never reported as N_NO_COVERAGE.

The per-allele computation, previously repeated in query, region, batch,
dump and annotate, now lives in QueryEngine._variant_stats. annotate
also excludes carriers of stored alleles when the requested allele is
absent from the database. The test oracle had the same residual and is
corrected; a multi-allelic cohort is added to the oracle tests.
Most positions hold one allele, where the pooled evidence equals the
row's own bitmaps. Pass None instead of building it, and index stored
alleles by position in annotate rather than scanning them per record.
… duplicates

normalize_vcf.sh ran bcftools norm without -m, so multi-allelic records
were kept whole and their indels were not trimmed per allele: the
2:136592357 CAAAAAAA>C,CAAAAAAAAAAAA record stored its insertion as
CAAAAAAA>CAAAAAAAAAAAA instead of C>CAAAAA, splitting one variant into
two representations across samples.

It also passed -d twice; the second value, "both", treats any two
indels at one position as duplicates, so once the record is split one
of the two alleles was discarded.

Use -m -both -d exact. bcftools refuses to combine -m and -d before
1.20, so the script now checks the version and exits with a clear
message on older releases.

Closes #39
@codecov-commenter

codecov-commenter commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.40%. Comparing base (313c4c7) to head (5629bf6).

Files with missing lines Patch % Lines
src/afquery/query.py 94.87% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #40      +/-   ##
==========================================
+ Coverage   88.69%   89.40%   +0.71%     
==========================================
  Files          22       22              
  Lines        3077     3077              
  Branches      482      488       +6     
==========================================
+ Hits         2729     2751      +22     
+ Misses        229      207      -22     
  Partials      119      119              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

query prints nothing for a technology that does not cover the position,
so the capture-index check now looks for the 'No variants found'
message. State that --min-covered is still evaluated per allele at build
time, while --min-quality-evidence counts carriers of any allele.
@dlopez-bioinfo
dlopez-bioinfo merged commit ae6f7f4 into master Sep 30, 2026
3 checks passed
@dlopez-bioinfo
dlopez-bioinfo deleted the fix/multiallelic-and-capture branch September 30, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: multi-allelic variant normalization

2 participants