f3-statistics are used to test if populations are admixed or to measure shared genetic drift between two populations relative to an outgroup.

This post explains the theory behind admixture f3-statistics and shows how to run admixture f3 tests with AdmixPy.

If you want to skip the theoretical part, jump to Running an admixture f3-statistic in AdmixPy.


What is an f3-statistic?

For three populations, the statistic is written as:

f3(A;B,C)=Ei[(pA,ipB,i)(pA,ipC,i)] f_3(A;B,C)=\mathbb{E}_i\left[(p_{A,i}-p_{B,i})(p_{A,i}-p_{C,i})\right]

Here, AA is in the target position. Populations BB and CC are the reference populations. The value pA,ip_{A,i} is the allele frequency in population AA at SNP ii. The expectation is an average across SNPs.

The statistic multiplies two allele-frequency differences. It therefore assesses whether AA differs from BB and CC in the same direction.

At a given SNP, the product is positive when the allele frequency in AA is higher than in both reference populations or lower than in both. It is negative when the allele frequency in AA lies between those of BB and CC. The f3-statistic averages these products across SNPs.


f3 derived from f2

The f2-statistic is the mean squared allele-frequency difference between two populations:

f2(X,Y)=E[(pXpY)2] f_2(X,Y)=E[(p_X-p_Y)^2]

Expanding the three pairwise f2 distances gives:

f3(A;B,C)=f2(A,B)+f2(A,C)f2(B,C)2 \boxed{ f_3(A;B,C)= \frac{f_2(A,B)+f_2(A,C)-f_2(B,C)}{2} }

Hence, the value is negative when:

f2(B,C)>f2(A,B)+f2(A,C) f_2(B,C)>f_2(A,B)+f_2(A,C)

This means that the two references are farther from each other than their combined distances to the target.

Example, the three f2 distances are:

f2(A,B)=0.04,f2(A,C)=0.05,f2(B,C)=0.12 f_2(A,B)=0.04, \qquad f_2(A,C)=0.05, \qquad f_2(B,C)=0.12

Then:

f3(A;B,C)=0.04+0.050.122=0.015 f_3(A;B,C)=\frac{0.04+0.05-0.12}{2}=-0.015

The target occupies an intermediate position between the two references in allele-frequency space. That is the basic geometry behind an admixture f3-statistic.

f3 can also be written as an f4-statistic with a repeated population:

f3(A;B,C)=f4(A,B;A,C) f_3(A;B,C)=f_4(A,B;A,C)

This shows that the f3 admixture test is a special case of an f4-statistic. f4 statistics can also be used to test admixture using four populations, a topic I will possibly cover in another post.


Why a negative f3 statistic detects admixture

Suppose target AA formed through admixture between BB and CC:

pA=αpB+(1α)pC+εA, p_A=\alpha p_B+(1-\alpha)p_C+\varepsilon_A,

where α\alpha is the ancestry proportion from BB, and εA\varepsilon_A represents drift accumulated after admixture.

Assuming this later drift is independent of the difference between the sources:

f3(A;B,C)=dAα(1α)f2(B,C) \boxed{ f_3(A;B,C)=d_A-\alpha(1-\alpha)f_2(B,C) }

where

dA=E[εA2]. d_A=E[\varepsilon_A^2].

The admixture term is negative, whereas subsequent drift in the target contributes positively. Therefore, f3 becomes negative when

α(1α)f2(B,C)>dA. \alpha(1-\alpha)f_2(B,C)>d_A.

This also explains why an admixed population does not necessarily produce a negative result. The estimate can stay positive when the ancestry contribution from one source is small, the sources are closely related (resulting in a small f2(B,C)f_2(B, C) distance; hence a smaller negative admixture term), or the target had substantial drift after admixture.

The tree interpretation

Consider a simple population tree with no admixture:

      A
      |
      | a
      |
      M
     / \
  b /   \ c
   /     \
  B       C

Here, MM is the branch point connecting the three populations. The values aa, bb, and cc represent the genetic drift accumulated along each branch.

On an additive tree, the f2f_2 distance between two populations is the sum of the branch lengths along the path connecting them. The path from AA to BB contains branches aa and bb, so

f2(A,B)=a+b. f_2(A,B)=a+b.

Likewise,

f2(A,C)=a+c f_2(A,C)=a+c

and

f2(B,C)=b+c. f_2(B,C)=b+c.

Now substitute these distances into the f2f_2 formula:

f3(A;B,C)=f2(A,B)+f2(A,C)f2(B,C)2. f_3(A;B,C) =\frac{f_2(A,B)+f_2(A,C)-f_2(B,C)}{2}. f3(A;B,C)=(a+b)+(a+c)(b+c)2=2a+b+cbc2=2a2=a \begin{aligned} f_3(A;B,C) &=\frac{(a+b)+(a+c)-(b+c)}{2} \\ &=\frac{2a+b+c-b-c}{2} \\ &=\frac{2a}{2} \\ &=a \end{aligned}

Therefore, f3(A;B,C)f_3(A;B,C) equals the drift along the branch connecting AA to MM. Because a branch length cannot be negative, f3f_3 cannot be negative on a strictly additive population tree. A significantly negative estimate therefore rejects this tree topology, and indicates that AA has ancestry related to both BB and CC.


Significance and Z-scores

The Z-score measures how many standard errors the estimate lies from zero:

Z=f^3SE(f^3) Z=\frac{\widehat f_3}{\operatorname{SE}(\widehat f_3)}

For an admixture test, Z<3Z<-3 is usually used to indicate a significantly negative f3 estimate.

AdmixPy also reports a two-sided p-value:

p=2Φ(Z) p=2\Phi(-|Z|)

A Z-score of 3-3 means that the f3 estimate is three standard errors below zero. AdmixPy reports a two-sided p-value of approximately 0.00270.0027 for this result. This indicates that such an extreme estimate would be unlikely if the true f3 value were zero. For the directional hypothesis f3<0f_3<0, the one-sided p-value is Φ(Z)\Phi(Z).


Running an admixture f3-statistic in AdmixPy

For installation instructions, see Introducing AdmixPy. Start Python (or set up a Python file) in the folder containing your genotype files. Then import the package:

import admixpy as ap

Set the dataset prefix:

prefix = "v66_compatibility"

Run one admixture f3-statistic with:

ap.f3(prefix, pop1="Japanese", pop2="Japan_Chiba_HG_Jomon", pop3="China_Shandong_Dinggong_LN")

This returns:

       pop1                  pop2                        pop3          est           se       z         p       n
0  Japanese  Japan_Chiba_HG_Jomon  China_Shandong_Dinggong_LN  -0.00926283  0.000606282  -15.28  1.07e-52  980456

The significantly negative result (z < -3) suggests that Japanese are admixed between Jomon Hunter-Gatherer-related ancestry and mainland East Asian Neolithic-farmer-related ancestry.

Another example which produces a significantly negative estimate:

ap.f3(prefix, pop1="Morocco_EN", pop2="Morocco_Iberomaurusian", pop3="Spain_EN")

Result:

         pop1                    pop2      pop3         est          se       z         p       n
0  Morocco_EN  Morocco_Iberomaurusian  Spain_EN  -0.0197945  0.00112779  -17.55  5.79e-69  879916

The significantly negative result suggests that Morocco Neolithic is admixed between Iberomaurusian-related ancestry and ancestry related to ANF-derived groups, here Spain Early Neolithic in particular.

These examples come from simpler Neolithic contexts. Often, even a population with expected admixture does not produce a significantly negative f3-statistic for reasons mentioned above.

Unexpected population pairs can produce significantly negative results. Testing a wide range of combinations rather than only what seems directly plausible is useful. aDNA studies often report admixture f3-statistics as large supplementary tables with many combinations of target and reference populations.

An example:

       pop1       pop2    pop3          est           se      z         p       n
0  Karelian  Norwegian  Korean  -0.00281868  0.000471994  -5.97  2.35e-09  274624

Here, the negative f3 value is statistically significant (z=-5.97). This implies that Karelians are admixed between ancestries related to northern Europeans and eastern Eurasians. Korean is used here only for demonstration; better sources for the Uralic-associated East Asian ancestry would be available.

AdmixPy returns the following columns:

  • est: the estimated f3-statistic;
  • se: its block-jackknife standard error;
  • z: the estimate divided by its standard error;
  • p: the two-sided p-value;
  • n: the number of contributing SNPs.

f3 does not quantify ancestry proportions. Methods such as qpAdm are needed to estimate ancestry proportions.

Testing several source pairs

AdmixPy can run many f3 tests at once. To do that, create a pandas data frame with one row for each population triplet and columns named pop1, pop2, and pop3. The same target can then be tested against several source pairs, or different targets can be tested against different references:

import pandas as pd

tests = pd.DataFrame({
    "pop1": ["Target", "Target", "Target"],
    "pop2": ["Reference_A", "Reference_A", "Reference_B"],
    "pop3": ["Reference_B", "Reference_C", "Reference_C"],
})

ap.f3(prefix, tests).sort_values("z")

The call returns one result for each row in tests. Sorting by z places the most negative statistics first.


Unnormalized and normalized f3 statistics in AdmixPy

For direct genotype data, ap.f3(...) returns a normalized statistic by default. It divides the corrected f3 numerator by a sample-size adjusted estimate of the target heterozygosity. Use outgroupmode=True to return the unnormalized f3 numerator. Passing apply_corr=False keeps SNPs with fewer than two allele observations, the resulting estimate is sampling-biased.

Setting apply_corr=False can be neccessary when a pseudohaploid singleton is used as the target (pop1) or as a repeated source because a finite-sample correction cannot be computed in those cases. A singleton used only as a distinct source can still be analyzed with apply_corr=True, even when pseudohaploid.

When ap.f3(...) is given an f2 cache or F2Blocks object, it returns unnormalized f3 values; outgroupmode has no effect in that case. This is equivalent in scale to direct-genotype f3 with outgroupmode=True.


Finite-sample correction

Population allele frequencies are estimated from a finite number of observed alleles. This creates sampling noise.

The target AA appears in both allele-frequency differences. For three distinct populations, AdmixPy subtracts this correction at each SNP:

δA,i=p^A,i(1p^A,i)cA,i1, \delta_{A,i}= \frac{\widehat p_{A,i}(1-\widehat p_{A,i})} {c_{A,i}-1},

where cA,ic_{A,i} is the observed allele count in population AA at SNP ii. The corrected SNP value is:

g^i=(p^A,ip^B,i)(p^A,ip^C,i)δA,i \widehat g_i= (\widehat p_{A,i}-\widehat p_{B,i}) (\widehat p_{A,i}-\widehat p_{C,i})-\delta_{A,i}

Because the correction contains cA,i1c_{A,i}-1, it requires at least two observed alleles in the repeated population. A pseudohaploid singleton provides only one observed allele, so this correction is undefined when that population is repeated. A singleton used only as a distinct source does not need this repeated-population correction.


Block jackknifing for the standard error

Nearby SNPs are correlated through linkage disequilibrium. They cannot be treated as fully independent observations. Doing so would make the standard error too small.

AdmixPy uses a delete-one-block jackknife. It leaves out one SNP block at a time and recalculates the statistic. For equal blocks, the standard error is:

SE2=B1Bb=1B(f^3,bf3,)2 \operatorname{SE}^2= \frac{B-1}{B} \sum_{b=1}^{B} (\widehat f_{3,-b}-\overline{f}_{3,-})^2

This formula describes the simpler resampling="nominal_blocks" setting, where blocks are weighted by their nominal sizes. AdmixPy’s default resampling="pairwise_counts" uses a generalized count-weighted jackknife based on the number of usable SNPs in each block.

AdmixPy reads genetic distances and physical positions from the .snp or .bim files. By default, blgsize defines blocks using a genetic-map distance of 0.05 cM. If no usable genetic map is available, AdmixPy falls back to 2-Mb physical blocks. Setting blgsize to 100 or more uses physical positions directly, with the value interpreted in base pairs. If neither genetic nor physical positions are available, each chromosome is treated as one block.


Summary

f3-statistics compress three pairwise population relationships into one value.

With the target in the first position, a significantly negative value is evidence of admixture. It shows that the target lies between the two references in allele-frequency space.

With a deep outgroup in the first position, the statistic measures shared drift between the other two populations. Larger values indicate more shared drift relative to that outgroup. Outgroup-f3 will be covered in a separate post in more detail.