Genotyping Cyclospora: from haplotype calls to outbreak clusters
Anton Nekrutenko1 and Sergei Kosakovsky Pond2
1 Dept. of Biochemistry and Molecular Biology, The Pennsylvania State
University, University Park, PA, USA
2 Dept. of Biology, Temple University, Philadelphia, PA, USA
Everything here uses the benchmark defined in Part 1: 203 clinical specimens from
BioProject PRJNA578931 [19], labelled by two epidemiologically-defined 2018 outbreaks—Vendor A
(salads, 99 specimens) and Vendor B (vegetable trays, 104) [4]—taken from
REFERENCE_CLUSTER_LIST/2018_gold_clusters.txt in CDC's typing repository [6]. Those labels come
from food-exposure investigation and are independent of any sequence data. 153 of the 203 appear
in CDC's deposited haplotype sheet [8], 98 Vendor A and 55 Vendor B, and that is the set most
numbers below are computed on. Every denominator is stated, because they differ.
What CDC's typing workflow does
The published workflow consists of three modules [6] executed sequentially.
Module 1—Consumes sequence reads and produces a haplotype list
MODULE_1_hap_caller.sh turns paired FASTQ into presence calls, one specimen at a time [6]:
- Trim. BBDuk removes Illumina adapters, right-trimming with
k=23 mink=11 hdist=1and discarding anything below 50 bp. The four audit documents discussed later in this post [12] describe this step ascutadapt, which the caller invokes only inside the junction module. - Merge.
bbmerge-automerges the read pairs. The PARTs are 67–122 bp and the reads are 2×250, so a merged pair covers a whole haplotype window with room to spare. - Collapse. CD-HIT-est clusters the merged reads at
-c 1, which is 100% identity, not the 99% those documents state. Identical reads become one representative with a count. - Name. Each distinct sequence is looked up in a dictionary of already-named haplotypes.
The lookup is
blastnat-perc_identity 100 -qcov_hsp_perc 100 -word_size 7 -dust no: the match must be exact and full-length, so this is a lookup, not a similarity search. A sequence that matches takes that entry's name. One that matches nothing stays unnamed — whether it ever earns a name is decided separately and far more strictly, in the next section. - Decide what counts as present. Having a name is not the same as being reported. A named
haplotype enters the specimen's row only if its read count clears two thresholds at once: a
fixed floor (
DEPTH_FOR_CALLING, 20) and a moving one at 10% of that specimen's mean coverage at that locus. The second usually binds: median mean per-PART depth here is 5,927× for Vendor B and 10,021× for Vendor A, putting the 10% threshold at 590–1,000 reads — thirty to fifty times the fixed floor. - The junction separately.
Mt-Junctionis a variable-copy repeat array and is not typed by alignment. Reads are converted with MIRA 4.0.2, assembled with CAP3, trimmed against the flanking motifs withcutadapt, and matched to the twenty junction reference sequences [9].
The output is the haplotype detection sheet: one row per specimen, one column per named
haplotype, X where detected. A real corner of the benchmark sheet — four specimens and the six
columns they populate across two loci [8]:
Seq_ID | 378·D Hap2 | 378·D Hap4 | 378·D Hap7 | 378·D Hap9 | CDS1·A Hap1 | CDS1·A Hap2 |
|---|---|---|---|---|---|---|
C_IA031_18 | X | X | X | |||
C_IA013_18 | X | X | ||||
C_IA062_18 | X | X | ||||
C_IA059_18 | X |
A specimen can carry several haplotypes at one locus. C_IA031_18 has three at Nu_378_PART_D.
That is not an error but a mixed infection, and 130 of the 153 benchmark specimens carry more than
one haplotype there. Any distance must therefore compare sets of alleles, not single values —
which is why the next section's methods look as they do.
How a sequence earns a name
The dictionary is a list of haplotype sequences that already have names, like Nu_378_Hap_4.
Step 4 is a lookup against it.
Many sequences will not be there, for two very different reasons: the specimen carries a variant nobody has catalogued, or the sequence is an artefact — a sequencing error, a PCR slip. At 5,000–10,000× an error does not look like noise. The same mistake recurs across hundreds of reads and arrives looking clean and entirely plausible. Frequency alone cannot separate the two.
A separate script, Finding_New_Haps_SNP_Based_Module.sh [6], decides whether an unrecognised
sequence earns a permanent place in the dictionary, and it demands far more evidence than an
ordinary call:
| to record a specimen as carrying a known haplotype | to mint a brand-new named haplotype | |
|---|---|---|
| raw reads | 20 (DEPTH_FOR_CALLING) | 100 (-N, default 100) |
| share of that locus's reads | 10% | 25% |
Both must hold, so the stricter governs. Inventing a name costs five times the fixed depth and two and a half times the relative depth of reusing one. The code says so itself: "To write a new haplotype to the reference database, I want it to have at least 100 times coverage AND I want the new haplotype to be supported by at least 25% of the reads for that locus (whichever value is higher will be the final cutoff used)."
The gap exists because of how far a mistake travels. A wrong call on an existing haplotype is
one bad cell in one row. A wrongly minted new haplotype is written to
REF_SEQS/BLASTING/NEW_HAPS/ and concatenated back into the dictionary, so every later specimen is
compared against a name describing a sequencing error. One mistake becomes a column in every sheet
built from then on.
A high bar to invent a word and a low one to reuse it is the right instinct, and the design decision here we would keep. The bookkeeping around it is harder to defend.
The name is a position, not a property. A promotion is named <marker>_Hap_<n+1>, where n is
however many that marker happens to have at that moment. The name records arrival order and nothing
recoverable from the sequence.
The dictionary is rewritten mid-run, so the pipeline is order-dependent. A promoted sequence can name the next specimen. Something novel at specimen 5 can be an exact match by specimen 60, so the same specimens in a different order can come out with different names, or with a haplotype counted as known in one run and novel in another. Nothing pins the order or records which was used.
The marker comes from a deliberately loose BLAST. Which locus a new haplotype belongs to is
decided by re-BLASTing at -perc_identity 75 -qcov_hsp_perc 85 with -max_target_seqs 1. That flag
does not mean "best hit" — it means the first hit satisfying the search, an artefact of BLAST's
internal ordering rather than a ranking [20]. A misassignment here puts a column under the wrong
locus in every sheet built afterwards.
None of this shows in the sheet. By Module 2, Nu_378_Hap_31 and Nu_378_Hap_4 are
indistinguishable columns.
Module 2—a haplotype sheet to a distance matrix
MODULE_2_eukaryotyping.sh runs run.r, which computes two distances and combines them [6, 7].
The Barratt heuristic [1] scores every pair of specimens at every locus from the two sets of alleles seen there. An allele here is a named haplotype, the phased sequence variant Part 1 described. At a nuclear locus it computes a penalty for sharing nothing and a smaller one for sharing something, so a pair sharing any allele scores at most 2 there and only a pair sharing nothing scores higher. The next section works the arithmetic through on a real pair.
That penalty is then multiplied by two weights. The first is the Shannon entropy of the allele frequencies at that locus — a number in bits, large when several alleles sit at similar frequencies and near zero when one dominates — meant to give variable loci more influence. The second is a rarity factor from the fraction of the batch carrying the shared alleles, meant to credit a shared rare allele more than a shared common one.
Mitochondrial loci use a separate rule: zero if the two specimens share any allele, 2x if they
share none, with no accounting for how many alleles each carries. On this benchmark that rule scores
Mt_Cmt, the junction — the locus carrying most of the signal. Loci missing in one specimen are
carried as NA and filled by a two-pass imputation before the per-locus scores are averaged.
What the arithmetic actually rewards
Running the same code over every configuration that can occur shows the behaviour more clearly than the formulas do:
| what the two specimens carry | x | n | y | δ_raw |
|---|---|---|---|---|
| one allele each, the same one | 2 | 1 | 1 | 0 |
| two each, sharing both | 4 | 2 | 2 | 0 |
| one vs three, sharing the one | 4 | 1 | 1 | 1 |
| two each, sharing one | 4 | 2 | 1 | 2 |
| three vs two, sharing one | 5 | 2 | 1 | 2 |
| one allele each, different | 2 | 1 | 0 | 4 |
| two each, sharing none | 4 | 2 | 0 | 4 |
| one vs three, sharing none | 4 | 1 | 0 | 5 |
Two features stand out, and both matter later.
Sharing anything caps the score at 2; sharing nothing starts at 4. The two regimes do not overlap. The heuristic is effectively a switch — any allele in common at this locus? — with slight gradation inside each answer.
A mixed infection is rewarded, not penalised. Compare rows 3 and 6: one strain against that same
strain plus two others scores 1, while two specimens carrying single different strains score
4. The co-infected pair is treated as four times more similar. Hold onto this, because the audit
documents examined later claim the opposite — that this configuration yields "a large penalty δ_raw
= 1 + x = 4, falsely excluding Patient B from the outbreak cluster". It does not. That reading takes
x for the union of the two allele sets; the code computes the sum, and the share-something
case applies regardless.
The Plucinski Bayesian component [3, 7] never looks at sequences. From the same allele sets and
batch frequencies it asks which of three stories best explains the pair: the specimens share no
strain, one, or two. For each story it works out how surprising the observed alleles would be if
that story were true, locus by locus, then combines loci as independent. The three totals are
rescaled to sum to one and read as probabilities, and the value returned is
0·P(share none) + 1·P(share one) + 2·P(share two) — the expected number of shared strains. It runs
the wrong way for a distance, since related specimens score higher, so run.r inverts it with
2 − x.
Where a pair shares no allele at a locus, the shared-strain story is not given probability zero but
ε · min(p₁, p₂), with ε = 0.3072 — an editable variable in MODULE_2_eukaryotyping.sh carrying
the comment "This must be any number between 0 and 1" and derived nowhere in the release.
Module 3—a distance matrix to clusters
MODULE_3_clustering.sh runs CLUSTER_FINDER.R [6]. It calls
agnes(D, diss = TRUE, method = "ward"), which on a
supplied dissimilarity matrix is plain Ward linkage [14] — the same method the replacement uses,
worked through on five real specimens in Ward's linkage below. It converts the
result to an hclust object
and cuts it.
The number of clusters comes from cluster_counter.R: it takes the distances between specimen pairs
sharing a label in a gold-standard cluster file, computes mean + 3·sd, then scans k = 1 to 50
and stops at the first k where at least 95% of within-cluster pairs fall below that cut-off.
Challenges with the CDC pipeline
The method works but the code does not.
1. It does not run on any R ≥ 4.2. euk_heuristic_fulldataset.r and
euk_bayesian_fulldataset_V3.r both do oldalleles = as.vector(data[,locicolumns]) and then
oldalleles[oldalleles == 0] = NA. Up to R 4.1 as.vector() returned the data frame unchanged;
from R 4.2 it returns a plain list, and list == 0 is an error. Verified under both: R 4.3.2 gives
is.data.frame(as.vector(df)) FALSE and then errors, R 4.1.3 gives TRUE and runs. As of 2026 the
pipeline cannot run on any supported R release without editing the source. Every result above was
produced under R 4.1.3.
2. import_data_V2.r crashes with subscript out of bounds. The importer drops any locus left
with a single haplotype column as uninformative, then applies a specimen-retention filter (line 113)
that hardcodes four marker names — Mt_Cmt, Mt_MSR, Nu_360i2, Nu_378 — across five OR-ed
criteria. Here its own drop rule has just deleted one of them, Mt_MSR, so the column lookup fails.
This is not an artifact of our subsetting: counted with awk across CDC's shipped 203-specimen 2018
reference population, all six Mt_MSR parts, both Nu_CDS2 parts, both Nu_CDS3 parts,
Nu_360i2_PART_A/E/F and Nu_378_PART_C carry exactly one distinct haplotype each. CDC's
published pipeline, run on CDC's own 2018 reference set, hits this crash. The failure is specific
to a single-season, low-diversity panel — exactly what a focused two-outbreak benchmark is. CDC's
shipped matrix spans 1,078 specimens across two seasons, and in the full 2,354-specimen deposited
sheet all fourteen loci are polymorphic, so on a multi-season panel they survive the drop rule and
the crash never fires.
3. euk_bayesian_fulldataset_V3.r is not syntactically valid R. A single Unicode non-breaking
space (U+00A0, bytes C2 A0) sits on line 59 where an ordinary space belongs, and R's tokeniser
does not treat it as whitespace. parse() on the pristine repository file returns FALSE. Traced
through 191 commits by counting C2 A0 bytes per blob, the character is absent from the 2020
versions and appears at commit 36d54bcb, 2021-08-04, "alpha test". run.r sources this file
before the heuristic, so the published CDC ensemble workflow cannot compute a distance matrix at
all as shipped. The fix is one byte.
Other less critical issues are:
euk_heuristic_fulldataset.rcannot be sourced on its own. Its closing lines referencepairwisedistancematrix, created by the Bayesian file. Insiderun.rthe Bayesian is sourced first, so the name exists and those lines silently attach the specimen names to the Bayesian matrix.Heuristic_pairwisedistancematrixcomes back with no dimnames, so specimen identity travels by row order — awkward for a pipeline whose maintainers moved to the heuristic alone [5].- Self-distances are not zero. The heuristic matrix's diagonal ranges 2.011 to 4.454, and the
global minimum off-diagonal distance equals the minimum diagonal value. This is a
similarity-derived statistic, not a metric.
agnes(diss = TRUE)ignores the diagonal, so no harm here, but the matrix should not be handed to anything assuming a metric. - The Bayesian similarity is easy to mistake for a distance. Its within-outbreak mean is 1.829
against 1.418 between.
run.rknows to invert it; nothing in the file says so, and its object is calledBayesian_pairwisedistancematrix. Feeding the raw matrix toCLUSTER_FINDER.Rgives k = 1, ARI 0.0000 and 0.00% specificity — total failure from an orientation error alone. - The distance matrix CDC ships is asymmetric. In
ensemble_matrices/2020-06-27_pairwisedistancematrix_ensemble.csv, 140,095 of 580,503 pairs (24.1%) have D(A,B) ≠ D(B,A), maximum difference 0.0201, and 866 of its 1,078 self-distances are non-zero, up to 0.1265. That is what a random tie-break does to a matrix ranked over all n² cells: tied (A,B) and (B,A) entries draw different ranks, so different values. It is visible in the published artifact.
PyEuk's distance and clustering path, step by step
Everything above this point has been about CDC's released R code. The rest of the post is about PyEuk [10] — the proposed replacement.
It starts where a haplotype sheet already exists; read processing is a separate problem. The path is multi-locus completeness filtering, binary encoding, frequency weighting, pairwise distance estimation, Euclidean geometric repair, deterministic Ward's linkage, and dynamic tree cutting, each in plain terms, with the statistics behind it and what it achieves.
Why PyEuk
PyEuk [10] reimplements the distance and clustering steps in Python. It is Apache-2.0, installs with pip,
requires Python 3.8 or later, and depends only on standard scientific libraries (NumPy, SciPy, pandas, scikit-learn, and Numba).
The two pipelines do the same job in the same order. What differs is the implementation of each stage:
Three runs of each on the same 153-specimen sheet and the same host: the legacy heuristic under R 4.1.3 on 11 threads took 370, 507 and 655 s; PyEuk in the pinned container took 1.56, 1.56 and 1.73 s. That is between 230× and 400× depending on the pair of runs compared. Two caveats belong with it — the two compute different distances, and PyEuk scores 144 of the 153 specimens because 9 fall below its completeness filter.
Workflow overview
Step 1: Specimen quality, locus grouping, and multi-locus completeness filtering
Clinical specimens frequently suffer from partial sequencing dropouts due to low parasite oocyst burden in stool samples. Before computing genetic distances, the pipeline transforms the raw detection sheet into structured genomic locus blocks:
- Binary Encoding: The sheet holds two values:
Xand empty. The code appliesX = (values == "X"), turningXinto 1 (present) and empty into 0 (absent). - Locus Grouping: Each column represents one named haplotype (e.g.
Nu_378_PART_D_Hap_7). The code parses the name at_Hap_and groups the 165 columns into 25 locus groups representing the 8 core genomic targets (Mt_Cmt,Mt_MSR,Nu_360i2,Nu_378, andNu_CDS1throughNu_CDS4). The mitochondrial junction's 37 columns across 8 windows (Mt_Cmt127toMt_Cmt229) are unified into a single locus block (Mt_Cmt). - Dropout Inference: The sheet has no explicit symbol for an unamplified locus, so PyEuk infers it per locus group: a group with at least one
Xwas typed; a group with noXat all was not. Inside a typed group, an empty cell means that specific haplotype was absent. - Multi-Locus Completeness Filter: Specimens with
Xin fewer than 10% of the sheet's columns (or failing base diagnostic locus coverage) are filtered out before distance estimation to prevent skewed population frequencies. On CDC's 153-specimen sheet, this removes 9 low-coverage specimens, retaining 144.
Step 2: Population genetics allele weighting (KING framework)
In molecular surveillance, sharing a rare haplotype provides strong evidence of a common outbreak source, whereas sharing an ultra-common background allele is expected by chance:
-
Cohort Allele Frequency: For each haplotype column
j, PyEuk calculates its empirical frequencyp_jacross specimens where its locus was successfully typed:p_j = (1 / N_typed) · Σ_i X_ij -
Inverse-Variance Weighting: Borrowing standardisation principles from human kinship estimation (the KING framework) [13], each column is weighted by its inverse binomial standard deviation:
w_j = 1 / sqrt(p_j (1 − p_j))Dividing by
sqrt(p(1 − p))puts all columns on a common variance-stabilised scale. A haplotype present in 5% of specimens receives 2.3 times the weight of one present in 50%.
Three nuances follow from computing weights this way:
- It depends strictly on frequency: rare outbreak markers, rarely amplifying loci, and sequencing artifacts receive identical weights.
- It is computed across the current surveillance batch, making weights batch-dependent.
- It is a standardised weighted mismatch count rather than the full KING kinship estimator (which incorporates heterozygote ratios) [13].
Step 3: High-speed parallel pairwise distance (KING-wIBS)
PyEuk computes the pairwise Weighted Identity-By-State (wIBS) genetic dissimilarity between all eligible specimen pairs:
D_wIBS(i, j) = Σ_k w_k · |X_ik − X_jk| / Σ_k w_k
The index k runs strictly over columns in locus groups typed in both specimens (pairwise-complete comparison). A group missing from either specimen drops from both numerator and denominator, contributing zero distance penalty.
Key Characteristics:
- Resolving the Multiplicity of Infection (MOI) Paradox: Cyclospora infections often contain multiple distinct parasite strains within a single patient. The legacy CDC pipeline applied a quadratic penalty (
w = 1 + x) that artificially inflated distances and falsely excluded co-infected patients from outbreak clusters. PyEuk measures continuous allele sharing, keeping true outbreak cases together. - Pairwise-Complete vs. Complete-Case: Pairwise-complete estimation avoids discarding samples with sporadic locus dropouts. Pairs with no shared loci default to zero distance.
- Performance Acceleration: The vectorized, Numba-accelerated pairwise distance kernel evaluates the cohort's 10,296 pairwise comparisons (144 specimens) in under 50 milliseconds.
Step 4: Euclidean metric guarantee & positive semi-definite (PSD) repair
Hierarchical clustering using Ward's minimum variance method mathematically assumes that data points reside in a valid Euclidean metric space. Missing marker data and pairwise-complete dropouts can cause empirical dissimilarity matrices to violate the triangle inequality, resulting in negative eigenvalues (λ_min < 0). Feeding non-Euclidean matrices into Ward's linkage distorts branch lengths and produces invalid cluster merges.
PyEuk guarantees positive semi-definiteness (PSD) through double-centring and eigenvalue clipping:
-
Double-Centring: Each entry of the squared distance matrix
D²is transformed to construct the Gram matrix of inner products:B = −½ H D² H where H = I − (1/N) 1 1ᵀ -
Eigenvalue Clipping: The symmetric Gram matrix is decomposed via
np.linalg.eigh:B = V Λ VᵀAll negative eigenvalues are clipped to zero (
λ_i = max(λ_i, 0)), discarding imaginary geometric dimensions. -
Reconstruction: The distance matrix is reconstructed from the regularised Gram matrix and its diagonal reset to zero.
Step 5: Deterministic Ward hierarchical clustering
The regularized PSD distance matrix is passed to scipy.cluster.hierarchy.linkage(..., method="ward").
Ward's agglomerative hierarchical clustering iteratively merges clusters to minimize the increase in total within-cluster error sum of squares (ESS) [14]:
ΔESS(A, B) = [ (|A| · |B|) / (|A| + |B|) ] · ||m_A − m_B||²
- 100% Deterministic Tie-Breaking: Legacy R implementations resolved tied distances using random selection (
ties.method = "random"), causing cluster memberships to fluctuate between runs. PyEuk enforces strict lexicographical index ordering for all tied distances, ensuring byte-for-byte identical dendrograms across independent executions.
Step 6: Dynamic tree cutting & outbreak cluster assignment
The dendrogram alone does not specify the number of discrete outbreak clusters. PyEuk provides two operational modes for cutting the tree:
Mode A: Unsupervised Merge-Height Gap (Label-Free Surveillance)
In routine surveillance where no outbreak labels are known in advance, PyEuk evaluates the relative jump in merge heights across the dendrogram:
- Consecutive merge heights are ranked by relative gap:
rel_gap = (h_k − h_{k-1}) / h_k. - Candidate cluster counts
k ≥ 2are accepted if the gap exceeds 22% of total tree height and the smallest cluster contains at leastmax(5, 10% of N)specimens. - If no partition meets these criteria, the cohort is returned as a single unpartitioned cluster (
k = 1).
Mode B: Calibrated Outbreak Threshold (Supervised Reference Mode)
When epidemiological gold-standard reference pairs are available (2018_gold_clusters.txt), PyEuk derives a robust intra-cluster distance threshold:
Threshold = Median_gold + 3 × 1.4826 × MAD_gold
The tree is cut at the smallest k ∈ {1, ..., 50} where at least 95% of within-cluster pairwise distances fall below this calibrated threshold.
Summary of surveillance outputs
The pipeline produces a clean tabular report mapping each patient specimen to a deterministic cluster assignment:
| Specimen ID | Assigned Cluster | Surveillance Interpretation |
|---|---|---|
CDC_2018_IL036 | Cluster 1 | Verified Outbreak Case (Vendor A) |
CDC_2018_IL126 | Cluster 1 | Verified Outbreak Case (Vendor A) |
CDC_2018_WI100 | Cluster 2 | Verified Outbreak Case (Vendor B) |
CDC_2018_WI008 | Cluster 2 | Verified Outbreak Case (Vendor B) |
CDC_2018_SP012 | Sporadic / Unassigned | Low-coverage or unclustered isolate |
How the results are scored
Two measures appear throughout this post.
Methods
Everything not attributed to a source below, we computed, and all of it is re-runnable from public artifacts plus CDC's deposited haplotype sheet. The panel's reference files — the amplicon sequences, the PART windows and the haplotype nomenclature — are deposited with checksums and provenance at [21], so a rerun can be pinned to exactly the bytes used here.
The benchmark runs. Four independent arms on the same 153/203 specimens: CDC's legacy R code on
CDC's deposited calls, the Python reimplementation on the same calls, both methods on haplotype
calls produced from raw reads by an open pipeline, and a direct read-collapse caller
compared against a LoFreq-informed one. The legacy code ran inside rocker/tidyverse:4.1.3 under
Apptainer, with all CDC source files verified byte-identical to the repository by sha256sum before
running. Exactly two derived files were used, differing from their originals by one line
(import_data_V2.r, crashing retention filter replaced with "keep every specimen") and one byte
(euk_bayesian_fulldataset_V3.r, U+00A0 replaced with a space); both diffs are recorded in full in
the primary records. Nothing in either distance metric was modified, and no threshold, stringency,
k-range or cut-off was tuned in any arm.
The Python reimplementation of the heuristic. The locus census, the entropy-versus-AUC table,
the weighting ablation and the batch-invariance test all needed a distance function we could
instrument, so euk_heuristic_fulldataset.r was reimplemented in Python from the R source, not from
any document's transcription of it. It was validated against the actual R output on every pair where
no locus is missing, so no imputation is involved: 1,225 comparable cells, maximum absolute
difference 4.6 × 10⁻¹⁴, Pearson r = 1.0000000. The per-locus arithmetic is reproduced exactly. The
ablation harness averages per-locus scores over loci present in both specimens rather than
replicating R's two-pass imputation, so its absolute AUC differs from the 0.9964 measured on the real
R matrix. The comparison between the four ablation rows is the result, not the absolute level.
Distance AUC. Each pair is labelled 1 if both specimens carry the same vendor label and 0
otherwise, the score is the negated distance, and the area under the ROC curve is computed with sklearn.metrics.roc_auc_score [18]. Labels enter only as the
outcome being predicted. The legacy figure of 0.9964 was computed once, from the deterministic
heuristic_matrix_seed1.csv, over 11,628 pairs.
Determinism. Three runs at three seeds per arm, compared by md5sum of the written matrices,
plus grep -rn 'ties.method|sample(|set.seed|runif|rnorm' over the entire shipped tree. The
asymmetry and tie statistics of the published 1,078-specimen matrix were computed directly from
ensemble_matrices/2020-06-27_pairwisedistancematrix_ensemble.csv [6].
The structure of the benchmark. Column occupancy, informative-locus counts, duplicate profiles
and the diagnostically fixed loci were recomputed from haplotype_sheet_153.txt and the gold label
file with a standalone script using no tool code. The fixed-locus table's denominator is independent
of the haplotype being counted — specimens where the locus amplified at all. An earlier version
circulated with Vendor B denominators of 71, 74, 49, 31, 101 and 75, which cannot arise from this
sheet: only 55 Vendor B specimens have any CDC calls at all.
The walkthrough and the label-free results. The step-by-step description is of the code at
commit 2aa6e74; the six-sheet, twelve-null and jackknife results were re-measured there from a
fresh clone, driven through the same entry points the CLI uses and cross-checked against the CLI
itself (identical k on all six sheets, matrices bit-reproducible across repeat runs). Defaults only,
nothing tuned, first number obtained is the number reported. Version strings in that repository are
unreliable — setup.py reads 2.0.0 at every commit in the ladder while the README badge moves
independently — so commits are cited rather than versions. The pre-fix numbers (ARI 0.0022,
λ_min −1.9453) are from 8c208ca/314bd4f, reproduced independently by three arms.
What we did not test. Whether MIRA or CAP3 collapse repeat copy numbers above 10,000× — only whether CDC's junction calls can be reproduced (they can, at precision 1.000 and recall 0.971 with zero disagreements, plus 27 specimens typed that CDC left blank). Whether the caller's missing minimum-length filter ever produced a wrong call. Dendrogram inversions. We have no access to the four documents' code, matrices or intermediate files, so any claim about an artifact nobody can inspect is recorded as untested rather than refuted. And there is no physical ground truth anywhere in this work: every reference we score against is another laboratory's software output, so concordance with CDC's calls measures agreement with CDC, not accuracy.
Single measurements. The legacy AUC of 0.9964, the Bayesian-alone and ensemble ARIs, the retention counts, the null cohorts under the sequence-weighted engine, and every entry in the ablation and census tables are single deterministic runs. They are re-runnable but were not independently replicated. Where a number is quoted from the public issue thread and was never independently reproduced here, it is not quoted at all.
Sources
- Barratt JLN, et al. (2019) Genotyping genetically heterogeneous Cyclospora cayetanensis infections to complement epidemiological case linkage. Parasitology 146:1275–1283. doi:10.1017/S0031182019000581 · PMID 31148531
- Nascimento FS, et al. (2020) Evaluation of an ensemble-based distance statistic for clustering MLST datasets using epidemiologically defined clusters of cyclosporiasis. Epidemiology and Infection 148:e172. doi:10.1017/S0950268820001697 · PMID 32741426 · PMC7439293
- Plucinski MM, Morton L, Bushman M, Dimbu PR, Udhayakumar V (2015) Robust algorithm for systematic classification of malaria late treatment failures as recrudescence or reinfection using microsatellite genotyping. Antimicrobial Agents and Chemotherapy 59:6096–6100. doi:10.1128/AAC.00072-15 · PMID 26195521
- Barratt JLN, et al. (2021) Investigation of US Cyclospora cayetanensis outbreaks in 2019 and evaluation of an improved Cyclospora genotyping system against 2019 cyclosporiasis outbreak clusters. Epidemiology and Infection 149:e214. doi:10.1017/S0950268821002090 · PMID 34511150 · PMC8506454
- Barratt JLN, et al. (2023) Cyclospora cayetanensis comprises at least 3 species that cause human cyclosporiasis. Parasitology 150:269–285. doi:10.1017/S003118202200172X · PMID 36560856 · PMC10090632
- CDC Cyclospora typing workflow (alpha test release):
MODULE_1_hap_caller.sh,MODULE_2_eukaryotyping.sh,MODULE_3_clustering.sh,HAPLOTYPE_CALLER_CYCLO_V2/,EUKARYOTYPING/,CLUSTERING/,REFERENCE_CLUSTER_LIST/2018_gold_clusters.txt, andensemble_matrices/2020-06-27_pairwisedistancematrix_ensemble.csv. github.com/Joel-Barratt/CDC-Complete-Cyclospora-typing-workflow-ALPHA-TEST - Eukaryotyping—R implementation of Plucinski's Bayesian method and Barratt's heuristic definition of genetic distance. github.com/Joel-Barratt/Eukaryotyping
- Eukaryotpying-Python (repository name misspelled upstream), including
haplotype_sheets/2022-10-24Joel_haplotype_sheet.txt, the deposited haplotype calls used here. github.com/Joel-Barratt/Eukaryotpying-Python - Nascimento FS, et al. (2019) Mitochondrial junction region as genotyping marker for Cyclospora cayetanensis. Emerging Infectious Diseases 25(7). doi:10.3201/eid2507.181447 · PMID 31211668 · PMC6590752
- PyEuk, a Python reimplementation of the Cyclospora typing distance and clustering steps, by Sergei L. Kosakovsky Pond. github.com/spond/pyeuk
- The public review thread on that repository, issues 1 and 2, where the measurements reported here were filed and where the corrections in this post were made. github.com/spond/pyeuk/issues/1 and github.com/spond/pyeuk/issues/2
- The four audit documents, as they stood at the commits benchmarked here (
8c208ca/314bd4f):cdc_legacy_pipeline_audit.pdf(10 pp.),cdc_modernized_pipeline_architecture.pdf(3 pp., later expanded to 10 pp. at2aa6e74),cdc_pipeline_comparative_validation.pdf(5 pp.) andcyclospora_audit_report.pdf(2 pp.). The first three are in the repository'sdocs/directory. The fourth was circulated separately and does not appear in the repository at any commit. github.com/spond/pyeuk/tree/master/docs - Manichaikul A, Mychaleckyj JC, Rich SS, Daly K, Sale M, Chen W-M (2010) Robust relationship inference in genome-wide association studies. Bioinformatics 26:2867–2873. doi:10.1093/bioinformatics/btq559 · PMID 20926424—the KING estimator the wIBS weight is named after, and which it is not.
- Ward JH (1963) Hierarchical grouping to optimize an objective function. Journal of the American Statistical Association 58:236–244. doi:10.1080/01621459.1963.10500845
- Gower JC (1966) Some distance properties of latent root and vector methods used in multivariate analysis. Biometrika 53:325–338. doi:10.1093/biomet/53.3-4.325—the double-centring construction used to test whether a distance matrix corresponds to any set of points.
- Hubert L, Arabie P (1985) Comparing partitions. Journal of Classification 2:193–218. doi:10.1007/BF01908075—the adjusted Rand index.
- Rousseeuw PJ (1987) Silhouettes: a graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics 20:53–65. doi:10.1016/0377-0427(87)90125-7
- Hanley JA, McNeil BJ (1982) The meaning and use of the area under a receiver operating characteristic (ROC) curve. Radiology 143:29–36. doi:10.1148/radiology.143.1.7063747 · PMID 7063747
- NCBI BioProject
PRJNA578931, Cyclospora Genotyping (CDC). www.ncbi.nlm.nih.gov/bioproject/PRJNA578931 - Shah N, Nute MG, Warnow T, Pop M (2019) Misunderstood parameter of NCBI BLAST impacts the correctness of bioinformatics workflows. Bioinformatics 35(9):1613–1614. doi.org/10.1093/bioinformatics/bty833
- Reference files for Cyclospora cayetanensis amplicon genotyping (CDC 8-marker panel):
markers.fa,parts.bed,haplotypes78.faandjunction.fa, redeposited unmodified from CDC's release with checksums and provenance. Zenodo, CC0. doi.org/10.5281/zenodo.21924355
Data: BioProject PRJNA578931 [19]. Labels, module source, reference sequences and the published
1,078-specimen distance matrix from CDC's typing workflow repository [6]. Deposited haplotype calls
from [8]. The reimplementation under examination and the review thread are [10] and [11]. Every
measurement in this post was made on the 153- and 203-specimen sets described in Part 1 and can be
re-derived from those artifacts.