Download the CMV ECOcluster and have a look. Break down TCRs and HLA-COclusters by HLA allele and class.
Published
July 24, 2026
Download the CMV ECOcluster (using code from this repo) and have a look.
1 Takeaways from this post:
91 HLA-COclusters are associated with 81 HLA alleles
HLA-COclusters range in size from 14 to 6,562 TCRs
More TCRs are associated with Class II alleles than Class I, in bigger HLA-COclusters
A handful of alleles have more than one HLA-COcluster. They’re almost all Class I.
Class I TCRs have slightly but significantly higher pGen than Class II TCRs
maybe due to greater statistical power in more-prevalent Class II alleles
2 Setup
Basic imports for data manipulation and plotting.
Code
# basic importsimport pandas as pdfrom matplotlib import pyplot as pltimport seaborn as sns# make plots look nicefrom cmvividly.plots.style import set_styleset_style()# make tables look nice and be interactive.# itables can be a little fiddly, so just comment this out if it gives troubleimport itablesitables.init_notebook_mode()# statisticsfrom scipy.stats import mannwhitneyu
Reminder before we get started: HLA-COclusters are subclusters of ECOclusters that are each associated with a single HLA-allele. ECOclusters are built by clustering the HLA-COclusters, by breadth correlation in repertoires, across HLA alleles.
3 Load the 2026 CMV ECOcluster
Load the dataset and display a sample of the rows (you can sort on the columns and search for values). The loader method I’m using adds a few extra columns.
Code
# load the CMV ECOcluster, downloading if not already downloaded.from cmvividly.data.access import load_cmv_ecocluster_2026pdf_cmv_ecocluster_2026 = load_cmv_ecocluster_2026()pdf_cmv_ecocluster_2026
Loading ITables v2.9.1 from the init_notebook_mode cell...
(need help?)
There are 91 HLA-COclusters associated with 81 HLA alleles, so some alleles have multiple HLA-COclusters. We’ll look at that in a bit.
5.1 Build a dataframe with one row per HLA-COcluster
Here’s an interactive table with one row per HLA-COcluster:
Code
from cmvividly.data.manipulation import extract_hlacoclusters_pdfpdf_hla_coclusters = extract_hlacoclusters_pdf(pdf_cmv_ecocluster_2026)print(f"Rows for {len(pdf_hla_coclusters)} HLA-COclusters")pdf_hla_coclusters
Rows for 91 HLA-COclusters
Loading ITables v2.9.1 from the init_notebook_mode cell...
(need help?)
There are more Class II-associated TCRs and HLA-COclusters. I’m not entirely sure, But I think this is due to greater statistical power to associate TCRs with Class II alleles (or heterodimers, or p groups), and I think that in turn is due to the greater prevalence of some Class II alleles.
5.3 Look at the number of TCRs per HLA-COcluster
Code
f, ax = plt.subplots()sns.histplot(pdf_hla_coclusters.n_tcrs, stat="count", ax=ax)ax.set_title("TCRs per HLA-COcluster")ax.set_xlabel("TCRs")pdf_hla_coclusters.n_tcrs.describe()
count 91.000000
mean 576.340659
std 943.726222
min 14.000000
25% 83.000000
50% 180.000000
75% 797.500000
max 6562.000000
Name: n_tcrs, dtype: float64
It varies enormously, from 14 to 6,562! Do HLA-COcluster sizes vary by HLA class?
Code
f, ax = plt.subplots()sns.boxplot(data=pdf_hla_coclusters, y="n_tcrs", x="hla_class", ax=ax)ax.set_title("TCRs per HLA-COcluster, by HLA class")ax.set_xlabel("TCRs")pdf_hla_coclusters.n_tcrs.describe()mwu_p = mannwhitneyu(pdf_hla_coclusters[pdf_hla_coclusters.hla_class =="ci"].n_tcrs, pdf_hla_coclusters[pdf_hla_coclusters.hla_class =="cii"].n_tcrs, alternative="two-sided").pvalueclass_is_higher = pdf_hla_coclusters[pdf_hla_coclusters.hla_class =="ci"].n_tcrs.mean() > pdf_hla_coclusters[pdf_hla_coclusters.hla_class =="cii"].n_tcrs.mean()print(f"Mean TCRs per HLA-COcluster is higher for {'Class I'if class_is_higher else'Class II'}. 2-sided Mann-Whitney U test p-value: {mwu_p:.3e}.")
Mean TCRs per HLA-COcluster is higher for Class II. 2-sided Mann-Whitney U test p-value: 1.332e-05.
They sure do! Class II-associated HLA-COclusters are starkly and significantly bigger than Class I.
5.4 Which HLA alleles have more than one HLA-COcluster? How many?
Interesting: it’s almost all Class I alleles, even though there are both more HLA-COclusters and more TCRs associated with Class II alleles than Class I alleles. That suggests more population substructure w.r.t. Class I responses. Maybe there’s more differentiation of the CD8 response by infection phase than there is in CD4?
5.5 Take a look at TCR generation probability (pGen)
Two TCRs have dramatically lower pGen than the rest. Let’s take those out, just for plotting:
Code
n_verylow_pgens =sum(pdf_cmv_ecocluster_2026.tcr_pgen <1e-20)print(f"TCRs with very low pGen (< 1e-20): {n_verylow_pgens}")pdf_notlow_pgen = pdf_cmv_ecocluster_2026[pdf_cmv_ecocluster_2026.tcr_pgen >=1e-20]print(f"Plotting on TCRs with pGen >= 1e-20: {len(pdf_notlow_pgen)} of {len(pdf_cmv_ecocluster_2026)} TCRs")
TCRs with very low pGen (< 1e-20): 2
Plotting on TCRs with pGen >= 1e-20: 52445 of 52447 TCRs
5.5.1 Look at the distribution of pGen, full ECOcluster.
count 5.244700e+04
mean 9.605719e-10
std 4.195277e-09
min 0.000000e+00
25% 2.970808e-11
50% 1.259289e-10
75% 5.217633e-10
max 1.710280e-07
Name: tcr_pgen, dtype: float64
That’s more meaningful when placed in context. As described in the manuscript, it’s notably narrower, with a higher median, then randomly selected TCRs from a large number of repertoires.
5.5.2 Does it differ by HLA class? Plot and test.
Code
f, ax = plt.subplots()sns.boxplot(x="hla_class", y="log10_tcr_pgen_eps", data=pdf_notlow_pgen, ax=ax)ax.set_title("Distribution of log10(pGen)\nby HLA class (pGen >= 1e-20)")f.set_size_inches(4, 3)mwu_p = mannwhitneyu(pdf_cmv_ecocluster_2026[pdf_cmv_ecocluster_2026.hla_class =="ci"].tcr_pgen, pdf_cmv_ecocluster_2026[pdf_cmv_ecocluster_2026.hla_class =="cii"].tcr_pgen, alternative="two-sided").pvalueclass_ii_is_higher = pdf_cmv_ecocluster_2026[pdf_cmv_ecocluster_2026.hla_class =="cii"].tcr_pgen.mean() > pdf_cmv_ecocluster_2026[pdf_cmv_ecocluster_2026.hla_class =="ci"].tcr_pgen.mean()higher_class ="Class II"if class_ii_is_higher else"Class I"print(f"2-sided Mann-Whitney U test p-value: {mwu_p:.3e}. Mean pGen is higher for {higher_class}.")
2-sided Mann-Whitney U test p-value: 1.220e-03. Mean pGen is higher for Class I.
The means aren’t very different, but Class I mean is significantly higher. That could be a consequence of the greater statistical power to associated TCRs with Class II alleles: In Class II, we can associate TCRs with lower pGen, that occur in a lower proportion of people with the allele.