Code
source("_setup.R")
spec <- read.csv("data/final_panel_spec.csv")
hap <- read.csv("data/haplotype_frequencies.csv")
af <- read.csv("data/panel_allele_frequencies.csv")Four haplotype terms, linkage structure and allele frequencies
November 11, 2026
The unfinished predecessor analysis simulated three PTX3 SNPs as independent multiplicative risks. They are in near-complete linkage disequilibrium and tag one h1/h2 haplotype, so independent simulation triple-counts a single signal. The same error would recur within CLEC7A and CD209 if their two SNPs each entered as additive terms. The panel therefore enters as four haplotype terms, with IL10 and IFNG as optional single SNPs. TLR9 rs352140 is dropped (one usable study of three).
| Term | Variants | Studies (union) | Independent cohorts | Study IDs |
|---|---|---|---|---|
| PTX3 haplotype | rs2305619, rs3816527, rs1840680 | 7 | 7 | S026, S029, S035, S045, S056, S077, S080 |
| TLR4 haplotype | rs4986790, rs4986791 | 5 | 5 | S046, S056, S061, S066, S077 |
| CLEC7A haplotype | rs16910526, rs7309123 | 11 | 9 | S023, S035, S045, S046, S056, S070, S071, S073, S074, S077, S080 |
| CD209 haplotype | rs4804800, rs7248637 | 5 | 4 | S021, S045, S071, S077, S080 |
| IL10 rs1800896 (optional) | rs1800896 | 3 | 3 | S003, S061, S077 |
| IFNG rs2069705 (optional) | rs2069705 | 3 | 3 | S046, S072, S080 |
Herrero-Sanchez 2018 is excluded from the primary analysis because its endpoint is Candida or Aspergillus detection or colonisation and not proven or probable invasive mould disease. The cohort counts above include it, and excluding it takes PTX3 from 7 to 6 and CLEC7A from 9 to 8.
Pairwise values in 1000 Genomes European samples.
| Gene | Locus 1 | Locus 2 | r² | D′ |
|---|---|---|---|---|
| PTX3 | rs2305619 | rs3816527 | 0.787 | 1.000 |
| PTX3 | rs2305619 | rs1840680 | 0.996 | 1.000 |
| PTX3 | rs3816527 | rs1840680 | 0.790 | 1.000 |
| TLR4 | rs4986790 | rs4986791 | 0.945 | 0.981 |
| CLEC7A | rs16910526 | rs7309123 | 0.104 | 1.000 |
| CD209 | rs4804800 | rs7248637 | 0.413 | 0.671 |
For CLEC7A, D′ = 1 means no recombination between the two sites. The rare Y238X allele (rs16910526) therefore sits entirely on one rs7309123 background, and the low r² reflects the frequency difference and not independence. For CD209, D′ = 0.67 and all four haplotypes are present.
Haplotype frequencies were derived from allele frequencies and D, assuming the minor alleles are in coupling (D positive). D′ near 1 fixes the size of D but not its sign. The reported r² and the allele frequencies fix |D| independently of that assumption, so the sign can be checked arithmetically: if even the largest possible negative D is smaller than the |D| that r² requires, negative phase is impossible.
phase <- hap |>
group_by(gene, locus_1, locus_2) |>
summarise(
r2 = first(r2),
p1 = sum(haplotype_freq_1000G_EUR[!allele_1 %in% c("major", "other")]),
p2 = sum(haplotype_freq_1000G_EUR[!allele_2 %in% c("major", "other")]),
.groups = "drop"
) |>
mutate(
required_D = sqrt(r2 * p1 * (1 - p1) * p2 * (1 - p2)),
ceiling_if_negative = pmin(p1 * p2, (1 - p1) * (1 - p2)),
negative_possible = required_D <= ceiling_if_negative + 1e-3
)
phase |>
transmute(Gene = gene, `Locus 1` = locus_1, `Locus 2` = locus_2,
`Required |D|` = required_D, `Ceiling if D < 0` = ceiling_if_negative,
`Negative phase possible` = ifelse(negative_possible, "yes", "no")) |>
gt() |>
fmt_number(c(`Required |D|`, `Ceiling if D < 0`), decimals = 4)| Gene | Locus 1 | Locus 2 | Required |D| | Ceiling if D < 0 | Negative phase possible |
|---|---|---|---|---|---|
| CD209 | rs4804800 | rs7248637 | 0.0678 | 0.0143 | no |
| CLEC7A | rs16910526 | rs7309123 | 0.0408 | 0.0298 | no |
| PTX3 | rs2305619 | rs1840680 | 0.2475 | 0.2078 | no |
| PTX3 | rs2305619 | rs3816527 | 0.2162 | 0.1814 | no |
| PTX3 | rs3816527 | rs1840680 | 0.2166 | 0.1810 | no |
| TLR4 | rs4986790 | rs4986791 | 0.0524 | 0.0033 | no |
All six pairs are consistent only with coupling. The check assumes r² and D′ were taken from the 1000 Genomes source and not back-calculated from the assumed D. The phased chromosomes below confirm it directly.
| Allele 1 | Allele 2 | Frequency |
|---|---|---|
| PTX3: rs2305619 x rs3816527 | ||
| A | C | 0.3976 |
| A | other | 0.0587 |
| other | C | 0.0000 |
| other | other | 0.5437 |
| PTX3: rs2305619 x rs1840680 | ||
| A | A | 0.4553 |
| A | other | 0.0010 |
| other | A | 0.0000 |
| other | other | 0.5437 |
| PTX3: rs3816527 x rs1840680 | ||
| C | A | 0.3976 |
| C | other | 0.0000 |
| other | A | 0.0577 |
| other | other | 0.5447 |
| TLR4: rs4986790 x rs4986791 | ||
| G | T | 0.0557 |
| G | other | 0.0010 |
| other | T | 0.0020 |
| other | other | 0.9413 |
| CLEC7A: rs16910526 x rs7309123 | ||
| C | G | 0.0706 |
| C | major | 0.0000 |
| major | G | 0.3519 |
| major | major | 0.5775 |
| CD209: rs4804800 x rs7248637 | ||
| G | A | 0.0821 |
| G | major | 0.0422 |
| major | A | 0.0332 |
| major | major | 0.8425 |
That file holds pairwise frequencies derived from allele frequencies and D. The phased frequencies in the next section supersede it.
These come directly from phased 1000 Genomes phase 3 European genotypes (503 samples, 1,006 chromosomes, via the Ensembl REST API), then are rescaled to the gnomAD v4 non-Finnish-European allele frequencies that the project uses as primary. Rescaling is by iterative proportional fitting, which keeps the odds-ratio structure of the observed table, moves only the allele frequencies, and works for three loci. A haplotype that is absent in 1000G stays absent.
Alleles are in the order rs2305619, rs3816527, rs1840680.
hp |>
filter(block == "PTX3_trio", n_chromosomes > 0) |>
arrange(desc(freq_1000G_EUR)) |>
transmute(Haplotype = haplotype, Chromosomes = n_chromosomes,
`1000G EUR` = freq_1000G_EUR, SE = se_1000G_EUR, `gnomAD NFE (rescaled)` = freq_gnomAD_NFE_rescaled) |>
gt() |>
fmt_number(c(`1000G EUR`, SE, `gnomAD NFE (rescaled)`), decimals = 4)| Haplotype | Chromosomes | 1000G EUR | SE | gnomAD NFE (rescaled) |
|---|---|---|---|---|
| G-A-G | 547 | 0.5437 | 0.0157 | 0.5180 |
| A-C-A | 400 | 0.3976 | 0.0154 | 0.4242 |
| A-A-A | 58 | 0.0577 | 0.0073 | 0.0564 |
| A-A-G | 1 | 0.0010 | 0.0010 | 0.0014 |
There are three common haplotypes, not two. rs2305619 A and rs1840680 A mark the same class of chromosome (r² = 0.996), which is A-C-A plus A-A-A. rs3816527 C splits that class, and A-C-A is about 87% of it. The single A-A-G chromosome is too rare to estimate; its rescaled frequency is fixed by the margins alone.
hp |>
filter(block %in% c("CLEC7A", "TLR4", "CD209")) |>
mutate(Block = paste0(block, ": ", gsub(";", " x ", loci))) |>
transmute(Block, Haplotype = haplotype, Chromosomes = n_chromosomes,
`1000G EUR` = freq_1000G_EUR, `gnomAD NFE (rescaled)` = freq_gnomAD_NFE_rescaled,
`gnomAD NFE (D′ kept)` = freq_gnomAD_NFE_dprime) |>
gt(groupname_col = "Block") |>
fmt_number(c(`1000G EUR`, `gnomAD NFE (rescaled)`, `gnomAD NFE (D′ kept)`), decimals = 4)| Haplotype | Chromosomes | 1000G EUR | gnomAD NFE (rescaled) | gnomAD NFE (D′ kept) |
|---|---|---|---|---|
| CLEC7A: rs16910526 x rs7309123 | ||||
| C-G | 71 | 0.0706 | 0.0771 | 0.0771 |
| A-G | 354 | 0.3519 | 0.3492 | 0.3492 |
| C-C | 0 | 0.0000 | 0.0000 | 0.0000 |
| A-C | 581 | 0.5775 | 0.5737 | 0.5737 |
| TLR4: rs4986790 x rs4986791 | ||||
| G-T | 56 | 0.0557 | 0.0608 | 0.0603 |
| A-T | 2 | 0.0020 | 0.0034 | 0.0039 |
| G-C | 1 | 0.0010 | 0.0006 | 0.0011 |
| A-C | 947 | 0.9414 | 0.9352 | 0.9347 |
| CD209: rs4804800 x rs7248637 | ||||
| G-A | 84 | 0.0835 | 0.0778 | 0.0750 |
| A-A | 32 | 0.0318 | 0.0257 | 0.0285 |
| G-G | 41 | 0.0408 | 0.0473 | 0.0501 |
| A-G | 849 | 0.8439 | 0.8492 | 0.8464 |
The two rescaling methods agree closely. The largest difference is about 0.003, in CD209, where D′ is 0.69. For CLEC7A they are identical because D′ = 1.
| Block | Loci | r² (1000G) | D′ (1000G) | r² (rescaled) | D′ (rescaled) |
|---|---|---|---|---|---|
| PTX3_a | rs2305619 x rs3816527 | 0.787 | 1.000 | 0.792 | 1.000 |
| PTX3_b | rs2305619 x rs1840680 | 0.996 | 1.000 | 0.994 | 1.000 |
| PTX3_c | rs3816527 x rs1840680 | 0.790 | 1.000 | 0.796 | 1.000 |
| CLEC7A | rs16910526 x rs7309123 | 0.104 | 1.000 | 0.112 | 1.000 |
| TLR4 | rs4986790 x rs4986791 | 0.946 | 0.981 | 0.933 | 0.989 |
| CD209 | rs4804800 x rs7248637 | 0.431 | 0.685 | 0.414 | 0.716 |
The phased r² reproduces the earlier table for PTX3, TLR4 and CLEC7A. For CD209 it is 0.431 against 0.414 before. In every pair the minor alleles are in coupling.
1000 Genomes European samples include a Finnish group (FIN), while gnomAD NFE excludes Finnish individuals. Rescaling assumes the linkage structure carries over and moves only the allele frequencies. Haplotypes seen on a handful of chromosomes are poorly estimated, and the table gives each standard error.
| rsID | Gene | Position | gnomAD NFE | 1000G EUR |
|---|---|---|---|---|
| rs16910526 | CLEC7A | 12:10118488 | A: 0.9229, C: 0.0771 | C: 0.0706, A: 0.9294 |
| rs7309123 | CLEC7A | 12:10119994 | C: 0.5737, G: 0.4263 | G: 0.4225, C: 0.5775 |
| rs2305619 | PTX3 | 3:157437072 | A: 0.482, G: 0.518 | A: 0.4563, G: 0.5437 |
| rs4986790 | TLR4 | 9:117713024 | G: 0.0614, A: 0.9386 | A: 0.9433, G: 0.0567 |
| rs4986791 | TLR4 | 9:117713324 | C: 0.9358, T: 0.0642 | T: 0.0577, C: 0.9423 |
| rs3816527 | PTX3 | 3:157437525 | A: 0.5758, C: 0.4242 | A: 0.6024, C: 0.3976 |
| rs4804800 | CD209 | 19:7740242 | A: 0.8749, G: 0.1251 | G: 0.1243, A: 0.8757 |
| rs7248637 | CD209 | 19:7742141 | G: 0.8965, A: 0.1035 | G: 0.8847, A: 0.1153 |
| rs1840680 | PTX3 | 3:157438240 | A: 0.4806, G: 0.5194 | A: 0.4553, G: 0.5447 |
| rs5743708 | TLR2 | 4:153705165 | A: 0.0294, G: 0.9706 | A: 0.0239, G: 0.9761 |
| rs1800896 | IL10 | 1:206773552 | T: 0.5134, C: 0.4866 | T: 0.5467, C: 0.4533 |
| rs2069705 | IFNG | 12:68161231 | A: 0.6624, G: 0.3379, C: 0.0 | A: 0.673, G: 0.327 |
| rs5743810 | TLR6 | 4:38828729 | A: 0.4217, G: 0.5783 | A: 0.4085, G: 0.5915 |
gnomAD gives rs1800896 as T/C where the literature writes A/G. It is the same variant on the other strand, and it is the most-cited IL10 promoter SNP in this field, so a natural place for an orientation error.
---
title: "Genetic panel"
subtitle: "Four haplotype terms, linkage structure and allele frequencies"
date: today
---
```{r}
#| label: setup
source("_setup.R")
spec <- read.csv("data/final_panel_spec.csv")
hap <- read.csv("data/haplotype_frequencies.csv")
af <- read.csv("data/panel_allele_frequencies.csv")
```
## Why haplotype terms
The unfinished predecessor analysis simulated three *PTX3* SNPs as independent multiplicative risks. They
are in near-complete linkage disequilibrium and tag one h1/h2 haplotype, so independent simulation
triple-counts a single signal. The same error would recur within *CLEC7A* and *CD209* if their two SNPs each
entered as additive terms. The panel therefore enters as four haplotype terms, with *IL10* and *IFNG* as
optional single SNPs. *TLR9* rs352140 is dropped (one usable study of three).
```{r}
#| label: tbl-panel
#| tbl-cap: "Final panel specification."
spec |>
mutate(variants = gsub(";", ", ", variants), study_ids = gsub(";", ", ", study_ids)) |>
transmute(Term = term, Variants = variants, `Studies (union)` = n_studies_union,
`Independent cohorts` = n_independent_cohorts, `Study IDs` = study_ids) |>
gt()
```
*Herrero-Sanchez 2018* is excluded from the primary analysis because its endpoint is *Candida* or
*Aspergillus* detection or colonisation and not proven or probable invasive mould disease. The cohort counts
above include it, and excluding it takes *PTX3* from 7 to 6 and *CLEC7A* from 9 to 8.
## Linkage disequilibrium
Pairwise values in 1000 Genomes European samples.
```{r}
#| label: tbl-ld
#| tbl-cap: "Pairwise linkage between the two loci of each block."
hap |>
distinct(gene, locus_1, locus_2, r2, d_prime) |>
transmute(Gene = gene, `Locus 1` = locus_1, `Locus 2` = locus_2, `r²` = r2, `D′` = d_prime) |>
gt() |>
fmt_number(c(`r²`, `D′`), decimals = 3)
```
For *CLEC7A*, D′ = 1 means no recombination between the two sites. The rare Y238X allele (rs16910526)
therefore sits entirely on one rs7309123 background, and the low r² reflects the frequency difference and
not independence. For *CD209*, D′ = 0.67 and all four haplotypes are present.
## Haplotype phase check
Haplotype frequencies were derived from allele frequencies and D, assuming the minor alleles are in
coupling (D positive). D′ near 1 fixes the size of D but not its sign. The reported r² and the allele
frequencies fix |D| independently of that assumption, so the sign can be checked arithmetically: if even the
largest possible negative D is smaller than the |D| that r² requires, negative phase is impossible.
```{r}
#| label: tbl-phase
#| tbl-cap: "Phase check. A negative D is possible only if the required |D| is no larger than the negative-phase ceiling."
phase <- hap |>
group_by(gene, locus_1, locus_2) |>
summarise(
r2 = first(r2),
p1 = sum(haplotype_freq_1000G_EUR[!allele_1 %in% c("major", "other")]),
p2 = sum(haplotype_freq_1000G_EUR[!allele_2 %in% c("major", "other")]),
.groups = "drop"
) |>
mutate(
required_D = sqrt(r2 * p1 * (1 - p1) * p2 * (1 - p2)),
ceiling_if_negative = pmin(p1 * p2, (1 - p1) * (1 - p2)),
negative_possible = required_D <= ceiling_if_negative + 1e-3
)
phase |>
transmute(Gene = gene, `Locus 1` = locus_1, `Locus 2` = locus_2,
`Required |D|` = required_D, `Ceiling if D < 0` = ceiling_if_negative,
`Negative phase possible` = ifelse(negative_possible, "yes", "no")) |>
gt() |>
fmt_number(c(`Required |D|`, `Ceiling if D < 0`), decimals = 4)
```
All six pairs are consistent only with coupling. The check assumes r² and D′ were taken from the 1000
Genomes source and not back-calculated from the assumed D. The phased chromosomes below confirm it directly.
## Haplotype frequencies
```{r}
#| label: tbl-hap
#| tbl-cap: "Two-locus haplotype frequencies, 1000 Genomes European. 'major' and 'other' denote the non-minor allele."
hap |>
mutate(pair = paste0(gene, ": ", locus_1, " x ", locus_2)) |>
transmute(pair, `Allele 1` = allele_1, `Allele 2` = allele_2, Frequency = haplotype_freq_1000G_EUR) |>
gt(groupname_col = "pair") |>
fmt_number(Frequency, decimals = 4)
```
That file holds pairwise frequencies derived from allele frequencies and D. The phased frequencies in the next
section supersede it.
## Phased haplotype frequencies
These come directly from phased 1000 Genomes phase 3 European genotypes (503 samples, 1,006 chromosomes,
via the Ensembl REST API), then are rescaled to the gnomAD v4 non-Finnish-European allele frequencies that the
project uses as primary. Rescaling is by iterative proportional fitting, which keeps the odds-ratio structure
of the observed table, moves only the allele frequencies, and works for three loci. A haplotype that is
absent in 1000G stays absent.
```{r}
#| label: setup-phased
hp <- read.csv("data/haplotype_frequencies_phased.csv")
ldp <- read.csv("data/haplotype_ld_phased.csv")
```
### PTX3, three loci
Alleles are in the order rs2305619, rs3816527, rs1840680.
```{r}
#| label: tbl-trio
#| tbl-cap: "PTX3 three-locus haplotypes. SE is the binomial standard error on 1,006 chromosomes."
hp |>
filter(block == "PTX3_trio", n_chromosomes > 0) |>
arrange(desc(freq_1000G_EUR)) |>
transmute(Haplotype = haplotype, Chromosomes = n_chromosomes,
`1000G EUR` = freq_1000G_EUR, SE = se_1000G_EUR, `gnomAD NFE (rescaled)` = freq_gnomAD_NFE_rescaled) |>
gt() |>
fmt_number(c(`1000G EUR`, SE, `gnomAD NFE (rescaled)`), decimals = 4)
```
There are three common haplotypes, not two. rs2305619 A and rs1840680 A mark the same class of chromosome
(r² = 0.996), which is A-C-A plus A-A-A. rs3816527 C splits that class, and A-C-A is about 87% of it. The
single A-A-G chromosome is too rare to estimate; its rescaled frequency is fixed by the margins alone.
### CLEC7A, TLR4 and CD209
```{r}
#| label: tbl-blocks
#| tbl-cap: "Two-locus haplotypes. 'D′ kept' rescales by preserving D′; the fitted values use iterative proportional fitting."
hp |>
filter(block %in% c("CLEC7A", "TLR4", "CD209")) |>
mutate(Block = paste0(block, ": ", gsub(";", " x ", loci))) |>
transmute(Block, Haplotype = haplotype, Chromosomes = n_chromosomes,
`1000G EUR` = freq_1000G_EUR, `gnomAD NFE (rescaled)` = freq_gnomAD_NFE_rescaled,
`gnomAD NFE (D′ kept)` = freq_gnomAD_NFE_dprime) |>
gt(groupname_col = "Block") |>
fmt_number(c(`1000G EUR`, `gnomAD NFE (rescaled)`, `gnomAD NFE (D′ kept)`), decimals = 4)
```
The two rescaling methods agree closely. The largest difference is about 0.003, in *CD209*, where D′ is
0.69. For *CLEC7A* they are identical because D′ = 1.
```{r}
#| label: tbl-ld-phased
#| tbl-cap: "Linkage in the phased data and after rescaling to gnomAD NFE margins."
ldp |>
filter(!is.na(r2_1000G)) |>
transmute(Block = block, Loci = gsub(";", " x ", loci),
`r² (1000G)` = r2_1000G, `D′ (1000G)` = dprime_1000G,
`r² (rescaled)` = r2_rescaled, `D′ (rescaled)` = dprime_rescaled) |>
gt() |>
fmt_number(where(is.numeric), decimals = 3)
```
The phased r² reproduces the earlier table for PTX3, TLR4 and CLEC7A. For *CD209* it is 0.431 against 0.414
before. In every pair the minor alleles are in coupling.
::: {.callout-note}
## Assumptions in the rescaling
1000 Genomes European samples include a Finnish group (FIN), while gnomAD NFE excludes Finnish individuals.
Rescaling assumes the linkage structure carries over and moves only the allele frequencies. Haplotypes seen
on a handful of chromosomes are poorly estimated, and the table gives each standard error.
:::
## Allele frequencies
```{r}
#| label: tbl-af
#| tbl-cap: "Allele frequencies for the 13 variants with three or more meta-analysable studies."
clean <- function(x) gsub(",", ", ", gsub("[{}\"]", "", x))
af |>
transmute(rsID = rsid, Gene = gene, Position = chr_pos,
`gnomAD NFE` = clean(gnomad_nfe_freqs), `1000G EUR` = clean(tg_eur_freqs)) |>
gt()
```
::: {.callout-note}
## rs1800896 is reported on the opposite strand
gnomAD gives rs1800896 as T/C where the literature writes A/G. It is the same variant on the other strand,
and it is the most-cited *IL10* promoter SNP in this field, so a natural place for an orientation error.
:::