Skip to content

diversity.pi gives wrong answer on multi-allelic sites (sum-of-int-values DAC) #100

Description

@andrewkern

Summary

diversity.pi (and the pi path through windowed_analysis) computes the
derived-allele count per variant as d = haplotypes.sum(axis=0). That works
when haplotype values are strictly {0, 1} (biallelic), but on a site whose
haplotype values include a 2 (triallelic under e.g. msprime's default
Jukes-Cantor mutation model), the int sum inflates d and the per-variant
pi falls back to a biallelic formula on incorrect counts.

Comparison to scikit-allel's allel.sequence_diversity shows ~1% per-window
discrepancy on data containing triallelic sites; on model="binary"
msprime data the two libraries agree to FP precision.

How to reproduce

import msprime
import allel
import cupy as cp
from pg_gpu import HaplotypeMatrix, diversity

# Default mutation model = jc69 -> can produce triallelic sites
ts = msprime.sim_ancestry(samples=20, sequence_length=100_000,
                          recombination_rate=1e-4, random_seed=42, ploidy=2)
ts = msprime.sim_mutations(ts, rate=1e-3, random_seed=42)
hm = HaplotypeMatrix.from_ts(ts)

# pg_gpu
pi_pg = diversity.pi(hm)

# scikit-allel
haps = cp.asnumpy(hm.haplotypes)
pos = cp.asnumpy(hm.positions)
ac = allel.HaplotypeArray(haps.T).count_alleles()
pi_a = allel.sequence_diversity(pos, ac)

print(f"pg_gpu: {pi_pg}, allel: {pi_a}, ratio: {pi_pg / pi_a}")
# observed: pg_gpu and allel disagree by ~1%

The same call with model="binary" produces matching values to FP precision.

What's actually wrong

pg_gpu/diversity.py:_site_contribution's 'pi' branch is:

return cp.where(seg, 2 * d * (n_safe - d) / (n_safe * (n_safe - 1)), 0.0)

This assumes d = count_of_derived_alleles and n_safe = total_haps. With
binary data, d = haps.sum(axis=0) is the derived-allele count. With
triallelic data, haps.sum(axis=0) mixes alt1 (+1) and alt2 (+2)
contributions and the resulting d no longer corresponds to a single
allele's count.

allel.mean_pairwise_difference(ac) handles the general case correctly
because ac carries one column per allele and the pairwise sum is over
all allele pairs.

Why it has been hiding

tests/test_scikit_allel_comparison.py constructs haplotype matrices by
hand with values restricted to {0, 1}, so count_alleles() returns
2-column ac and the formulas trivially agree. The discrepancy only
shows up against msprime data with the default JC mutation model, which
came up while building a test_streaming_vs_allel.py parity test for
streaming-from-zarr work.

Suggested fix

Replace the haps.sum(axis=0) DAC with a per-allele count from the
underlying (n_var, n_dip, 2) genotype block (or extend
HaplotypeMatrix to track per-allele counts directly). The pi formula
becomes a sum-over-allele-pairs the same way
allel.mean_pairwise_difference handles it. Same change applies to
other diversity / divergence statistics that derive a "derived-allele
count" from the int sum (theta_h, theta_l, fay_wu_h,
zeng_e, etc.).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions